1use crate::{
2 AccidentalMark, BeamGroup, ConcertKeyOverride, CourtesyAccidental, LayoutConfig, LayoutResult,
3 RowLayout, SourceVoiceAddress, SpanMark, TupletGroup,
4};
5use acorde_core::{BeamState, HairpinKind, NoteAddr, OttavaKind, Score, Step, TupletInfo};
6use std::collections::HashMap;
7
8fn transpose_key_fifths(fifths: i8, semitones: i8) -> i8 {
13 let raw = (semitones as i16 * 7).rem_euclid(12) as i8;
14 let delta = if raw > 6 { raw - 12 } else { raw };
15 (fifths + delta).clamp(-7, 7)
16}
17
18pub fn compute_layout(score: &Score, config: &LayoutConfig) -> LayoutResult {
22 let per_row = config.measures_per_row.max(1);
23 let first_row = config.first_row_measures.map(|n| n.max(1));
24 let vis_slots = build_vis_slots(score);
25 let rows = build_rows(score, &vis_slots, per_row, first_row);
26 let spans = resolve_spans(score);
27
28 let mut concert_key_overrides = Vec::new();
29 if config.concert_pitch {
30 let base_fifths = score.settings.key_signature.fifths;
31 for (pi, part) in score.parts.iter().enumerate() {
32 for (si, staff) in part.staves.iter().enumerate() {
33 if staff.transpose_semitones != 0 {
34 concert_key_overrides.push(ConcertKeyOverride {
35 part_index: pi,
36 staff_index: si,
37 fifths: transpose_key_fifths(base_fifths, staff.transpose_semitones),
38 });
39 }
40 }
41 }
42 }
43
44 let beam_groups = collect_beam_groups(score);
45 let tuplet_groups = collect_tuplet_groups(score);
46 let courtesy_accidentals = collect_courtesy_accidentals(score);
47 let accidentals = collect_accidental_marks(score);
48 let source_voice_addresses = collect_source_voice_addresses(score);
49
50 LayoutResult {
51 vis_slots,
52 rows,
53 spans,
54 typed_spanners: score.spanners.clone(),
55 concert_key_overrides,
56 beam_groups,
57 tuplet_groups,
58 courtesy_accidentals,
59 accidentals,
60 source_voice_addresses,
61 }
62}
63
64fn collect_source_voice_addresses(score: &Score) -> Vec<SourceVoiceAddress> {
65 let mut addresses = Vec::new();
66 for (part, part_value) in score.parts.iter().enumerate() {
67 for (staff, staff_value) in part_value.staves.iter().enumerate() {
68 for (measure, measure_value) in staff_value.measures.iter().enumerate() {
69 for (voice, source_voice_number) in
70 measure_value.source_voice_numbers.iter().enumerate()
71 {
72 if let Some(source_voice_number) = source_voice_number {
73 addresses.push(SourceVoiceAddress {
74 part,
75 staff,
76 measure,
77 voice,
78 source_voice_number: *source_voice_number,
79 });
80 }
81 }
82 }
83 }
84 }
85 addresses
86}
87
88fn build_vis_slots(score: &Score) -> Vec<usize> {
91 let measure_count = score
92 .parts
93 .first()
94 .and_then(|p| p.staves.first())
95 .map(|s| s.measures.len())
96 .unwrap_or(0);
97
98 let mut slots = Vec::new();
99 for phys in 0..measure_count {
100 let count = multi_rest_count(score, phys).max(1);
101 for _ in 0..count {
102 slots.push(phys);
103 }
104 }
105 slots
106}
107
108fn multi_rest_count(score: &Score, measure_index: usize) -> usize {
109 score
110 .parts
111 .iter()
112 .flat_map(|p| p.staves.iter())
113 .filter_map(|s| s.measures.get(measure_index))
114 .filter_map(|m| m.multi_rest_count)
115 .map(|c| c as usize)
116 .max()
117 .unwrap_or(1)
118}
119
120fn force_break_after(score: &Score, phys: usize) -> bool {
123 score
124 .parts
125 .iter()
126 .flat_map(|p| p.staves.iter())
127 .filter_map(|s| s.measures.get(phys))
128 .any(|m| m.system_break || m.page_break)
129}
130
131fn build_rows(
132 score: &Score,
133 vis_slots: &[usize],
134 per_row: usize,
135 first_row_limit: Option<usize>,
136) -> Vec<RowLayout> {
137 if vis_slots.is_empty() {
138 return vec![RowLayout {
139 measure_indices: Vec::new(),
140 }];
141 }
142
143 let mut rows: Vec<RowLayout> = Vec::new();
144 let mut current: Vec<usize> = Vec::new();
145 let mut current_width = 0usize;
146 let mut last_phys = usize::MAX;
147
148 for &phys in vis_slots {
149 if phys != last_phys {
150 let limit = if rows.is_empty() {
151 first_row_limit.unwrap_or(per_row)
152 } else {
153 per_row
154 };
155 let span_width = multi_rest_count(score, phys).max(1);
156 if !current.is_empty() && current_width.saturating_add(span_width) > limit {
157 rows.push(RowLayout {
158 measure_indices: std::mem::take(&mut current),
159 });
160 current_width = 0;
161 }
162 current.push(phys);
163 current_width = current_width.saturating_add(span_width);
164 if force_break_after(score, phys) {
165 rows.push(RowLayout {
166 measure_indices: std::mem::take(&mut current),
167 });
168 current_width = 0;
169 }
170 }
171 last_phys = phys;
172 }
173
174 if !current.is_empty() {
175 rows.push(RowLayout {
176 measure_indices: current,
177 });
178 }
179 rows
180}
181
182fn resolve_spans(score: &Score) -> Vec<SpanMark> {
185 let mut spans = Vec::new();
186
187 for (pi, part) in score.parts.iter().enumerate() {
188 for (si, staff) in part.staves.iter().enumerate() {
189 for voice in 0..4usize {
190 let mut open_hairpin: Option<(NoteAddr, HairpinKind)> = None;
191 let mut open_ottava: Option<(NoteAddr, OttavaKind)> = None;
192 let mut open_pedal: Option<NoteAddr> = None;
193 let mut open_slur: Option<NoteAddr> = None;
194 let mut open_trill_line: Option<NoteAddr> = None;
195 let mut open_glissando: Option<NoteAddr> = None;
196
197 for (mi, measure) in staff.measures.iter().enumerate() {
198 for (ni, note) in measure.voices[voice].iter().enumerate() {
199 let addr = NoteAddr {
200 part: pi,
201 staff: si,
202 measure: mi,
203 voice,
204 note: ni,
205 };
206
207 if let Some(kind) = note.hairpin_start {
208 open_hairpin = Some((addr.clone(), kind));
209 }
210 if note.hairpin_end
211 && let Some((start, kind)) = open_hairpin.take()
212 {
213 spans.push(SpanMark::Hairpin {
214 kind,
215 start,
216 end: addr.clone(),
217 });
218 }
219
220 if let Some(kind) = note.ottava_start {
221 open_ottava = Some((addr.clone(), kind));
222 }
223 if note.ottava_end
224 && let Some((start, kind)) = open_ottava.take()
225 {
226 spans.push(SpanMark::Ottava {
227 kind,
228 start,
229 end: addr.clone(),
230 });
231 }
232
233 if note.pedal_start {
234 open_pedal = Some(addr.clone());
235 }
236 if note.pedal_end
237 && let Some(start) = open_pedal.take()
238 {
239 spans.push(SpanMark::Pedal {
240 start,
241 end: addr.clone(),
242 });
243 }
244
245 if note.slur_start {
246 open_slur = Some(addr.clone());
247 }
248 if note.slur_end
249 && let Some(start) = open_slur.take()
250 {
251 spans.push(SpanMark::Slur {
252 start,
253 end: addr.clone(),
254 });
255 }
256
257 if note.trill_line_start {
258 open_trill_line = Some(addr.clone());
259 }
260 if note.trill_line_end
261 && let Some(start) = open_trill_line.take()
262 {
263 spans.push(SpanMark::TrillLine {
264 start,
265 end: addr.clone(),
266 });
267 }
268
269 if note.glissando_start {
270 open_glissando = Some(addr.clone());
271 }
272 if note.glissando_end
273 && let Some(start) = open_glissando.take()
274 {
275 spans.push(SpanMark::Glissando {
276 start,
277 end: addr.clone(),
278 });
279 }
280
281 if let Some(chord) = ¬e.chord_symbol
282 && let Some(end) = &chord.range_end
283 {
284 spans.push(SpanMark::Harmony {
285 label: chord.display_text(),
286 start: addr.clone(),
287 end: end.clone(),
288 });
289 }
290 }
291 }
292 }
293 }
294 }
295 spans
296}
297
298fn collect_beam_groups(score: &Score) -> Vec<BeamGroup> {
301 let mut groups = Vec::new();
302 for (pi, part) in score.parts.iter().enumerate() {
303 for (si, staff) in part.staves.iter().enumerate() {
304 for (mi, measure) in staff.measures.iter().enumerate() {
305 for vi in 0..4usize {
306 let voice = &measure.voices[vi];
307 let mut current: Option<Vec<usize>> = None;
308
309 for (ni, note) in voice.iter().enumerate() {
310 match note.beam {
311 BeamState::Begin => {
312 current = Some(vec![ni]);
313 }
314 BeamState::BeginEnd => {
315 groups.push(BeamGroup {
316 part: pi,
317 staff: si,
318 measure: mi,
319 voice: vi,
320 note_indices: vec![ni],
321 });
322 }
323 BeamState::Continue
324 | BeamState::ForwardHook
325 | BeamState::BackwardHook => {
326 if let Some(ref mut g) = current {
327 g.push(ni);
328 }
329 }
330 BeamState::End => {
331 if let Some(mut g) = current.take() {
332 g.push(ni);
333 groups.push(BeamGroup {
334 part: pi,
335 staff: si,
336 measure: mi,
337 voice: vi,
338 note_indices: g,
339 });
340 }
341 }
342 BeamState::None => {
343 current = None;
344 }
345 }
346 }
347 }
348 }
349 }
350 }
351 groups
352}
353
354fn collect_tuplet_groups(score: &Score) -> Vec<TupletGroup> {
357 let mut groups = Vec::new();
358 for (pi, part) in score.parts.iter().enumerate() {
359 for (si, staff) in part.staves.iter().enumerate() {
360 for (mi, measure) in staff.measures.iter().enumerate() {
361 for vi in 0..4usize {
362 let voice = &measure.voices[vi];
363 let mut current_indices: Vec<usize> = Vec::new();
364 let mut current_info: Option<TupletInfo> = None;
365
366 let flush = |indices: &mut Vec<usize>,
367 info: &mut Option<TupletInfo>,
368 groups: &mut Vec<TupletGroup>| {
369 if indices.len() >= 2
370 && let Some(ti) = info.take()
371 {
372 groups.push(TupletGroup {
373 part: pi,
374 staff: si,
375 measure: mi,
376 voice: vi,
377 note_indices: std::mem::take(indices),
378 actual_notes: ti.actual_notes,
379 normal_notes: ti.normal_notes,
380 });
381 return;
382 }
383 indices.clear();
384 info.take();
385 };
386
387 for (ni, note) in voice.iter().enumerate() {
388 match ¬e.tuplet {
389 Some(ti) => {
390 let same_group = current_info.as_ref().is_some_and(|prev| {
391 prev.actual_notes == ti.actual_notes
392 && prev.normal_notes == ti.normal_notes
393 });
394 if !same_group {
395 flush(&mut current_indices, &mut current_info, &mut groups);
396 current_info = Some(ti.clone());
397 }
398 current_indices.push(ni);
399 }
400 None => {
401 flush(&mut current_indices, &mut current_info, &mut groups);
402 }
403 }
404 }
405 flush(&mut current_indices, &mut current_info, &mut groups);
406 }
407 }
408 }
409 }
410 groups
411}
412
413fn step_idx(step: &Step) -> u8 {
414 match step {
415 Step::C => 0,
416 Step::D => 1,
417 Step::E => 2,
418 Step::F => 3,
419 Step::G => 4,
420 Step::A => 5,
421 Step::B => 6,
422 }
423}
424
425fn key_alter(fifths: i8, step: &Step) -> i8 {
427 const SHARPS: [Step; 7] = [
428 Step::F,
429 Step::C,
430 Step::G,
431 Step::D,
432 Step::A,
433 Step::E,
434 Step::B,
435 ];
436 const FLATS: [Step; 7] = [
437 Step::B,
438 Step::E,
439 Step::A,
440 Step::D,
441 Step::G,
442 Step::C,
443 Step::F,
444 ];
445 if fifths > 0 && SHARPS[..fifths as usize].contains(step) {
446 1
447 } else if fifths < 0 && FLATS[..(-fifths) as usize].contains(step) {
448 -1
449 } else {
450 0
451 }
452}
453
454fn collect_courtesy_accidentals(score: &Score) -> Vec<CourtesyAccidental> {
455 let mut result = Vec::new();
456 let base_fifths = score.settings.key_signature.fifths;
457
458 for (pi, part) in score.parts.iter().enumerate() {
459 for (si, staff) in part.staves.iter().enumerate() {
460 let mut prev_alters: HashMap<(u8, i8), i8> = HashMap::new();
462 let mut current_fifths = base_fifths;
463
464 for (mi, measure) in staff.measures.iter().enumerate() {
465 if let Some(key_sig) = measure.key_sig.as_ref() {
466 current_fifths = key_sig.fifths;
467 prev_alters.clear();
468 }
469
470 for vi in 0..4usize {
472 for (ni, note) in measure.voices[vi].iter().enumerate() {
473 if note.tie_end {
474 continue;
475 }
476 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
477 let key = (step_idx(&pitch.step), pitch.octave);
478 if prev_alters.contains_key(&key) {
479 result.push(CourtesyAccidental {
480 part: pi,
481 staff: si,
482 measure: mi,
483 voice: vi,
484 note_index: ni,
485 pitch_index: pitch_idx,
486 alter: pitch.alter,
487 });
488 }
489 }
490 }
491 }
492
493 prev_alters.clear();
495 for vi in 0..4usize {
496 for note in measure.voices[vi].iter() {
497 for pitch in note.pitches.iter() {
498 if pitch.alter != key_alter(current_fifths, &pitch.step) {
499 prev_alters
500 .insert((step_idx(&pitch.step), pitch.octave), pitch.alter);
501 }
502 }
503 }
504 }
505 }
506 }
507 }
508 result
509}
510
511fn collect_accidental_marks(score: &Score) -> Vec<AccidentalMark> {
520 let mut result = Vec::new();
521 let base_fifths = score.settings.key_signature.fifths;
522
523 for (pi, part) in score.parts.iter().enumerate() {
524 for (si, staff) in part.staves.iter().enumerate() {
525 let mut current_fifths = base_fifths;
526
527 for (mi, measure) in staff.measures.iter().enumerate() {
528 if let Some(key_sig) = measure.key_sig.as_ref() {
529 current_fifths = key_sig.fifths;
530 }
531
532 let mut active: HashMap<(u8, i8), i8> = HashMap::new();
534
535 for vi in 0..4usize {
536 for (ni, note) in measure.voices[vi].iter().enumerate() {
537 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
538 let key = (step_idx(&pitch.step), pitch.octave);
539 let baseline = active
540 .get(&key)
541 .copied()
542 .unwrap_or_else(|| key_alter(current_fifths, &pitch.step));
543 if pitch.alter != baseline {
544 if !note.tie_end {
545 result.push(AccidentalMark {
546 part: pi,
547 staff: si,
548 measure: mi,
549 voice: vi,
550 note_index: ni,
551 pitch_index: pitch_idx,
552 alter: pitch.alter,
553 });
554 }
555 active.insert(key, pitch.alter);
556 }
557 }
558 }
559 }
560 }
561 }
562 }
563 result
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569 use acorde_core::{Clef, Measure, Part, Score, Staff};
570
571 fn score_with_measures(n: usize) -> Score {
572 let mut score = Score::default();
573 score.parts.clear();
574 let mut staff = Staff::new(Clef::Treble);
575 for i in 0..n {
576 let mut m = Measure::empty(4, 4);
577 m.number = i as u32 + 1;
578 staff.measures.push(m);
579 }
580 let mut part = Part::new("P1", "P1");
581 part.staves.push(staff);
582 score.parts.push(part);
583 score
584 }
585
586 #[test]
587 fn default_score_has_one_row_of_four() {
588 let score = Score::default();
590 let result = compute_layout(&score, &LayoutConfig::default());
591 assert_eq!(result.rows.len(), 1);
592 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
593 assert_eq!(result.vis_slots.len(), 4);
594 assert!(result.spans.is_empty());
595 }
596
597 #[test]
598 fn vis_slots_expand_multi_rest() {
599 let mut score = score_with_measures(1);
600 score.parts[0].staves[0].measures[0].multi_rest_count = Some(4);
601
602 let result = compute_layout(
603 &score,
604 &LayoutConfig {
605 measures_per_row: 8,
606 concert_pitch: false,
607 first_row_measures: None,
608 },
609 );
610 assert_eq!(result.vis_slots.len(), 4);
611 assert!(result.vis_slots.iter().all(|&v| v == 0));
612 }
613
614 #[test]
615 fn measures_split_into_rows() {
616 let score = score_with_measures(6);
617 let result = compute_layout(
618 &score,
619 &LayoutConfig {
620 measures_per_row: 4,
621 concert_pitch: false,
622 first_row_measures: None,
623 },
624 );
625 assert_eq!(result.rows.len(), 2);
626 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
627 assert_eq!(result.rows[1].measure_indices, vec![4, 5]);
628 }
629
630 #[test]
631 fn single_measure_single_row() {
632 let score = score_with_measures(1);
633 let result = compute_layout(
634 &score,
635 &LayoutConfig {
636 measures_per_row: 4,
637 concert_pitch: false,
638 first_row_measures: None,
639 },
640 );
641 assert_eq!(result.rows.len(), 1);
642 assert_eq!(result.rows[0].measure_indices, vec![0]);
643 }
644
645 #[test]
646 fn exactly_full_rows() {
647 let score = score_with_measures(8);
648 let result = compute_layout(
649 &score,
650 &LayoutConfig {
651 measures_per_row: 4,
652 concert_pitch: false,
653 first_row_measures: None,
654 },
655 );
656 assert_eq!(result.rows.len(), 2);
657 assert_eq!(result.rows[0].measure_indices.len(), 4);
658 assert_eq!(result.rows[1].measure_indices.len(), 4);
659 }
660
661 #[test]
662 fn concert_pitch_bb_clarinet_transposes_key() {
663 let mut score = score_with_measures(1);
666 score.parts[0].staves[0].transpose_semitones = -2;
667 let result = compute_layout(
668 &score,
669 &LayoutConfig {
670 measures_per_row: 4,
671 concert_pitch: true,
672 first_row_measures: None,
673 },
674 );
675 assert_eq!(result.concert_key_overrides.len(), 1);
676 assert_eq!(result.concert_key_overrides[0].part_index, 0);
677 assert_eq!(result.concert_key_overrides[0].staff_index, 0);
678 assert_eq!(result.concert_key_overrides[0].fifths, -2);
679 }
680
681 #[test]
682 fn concert_pitch_false_no_override() {
683 let mut score = score_with_measures(1);
684 score.parts[0].staves[0].transpose_semitones = -2;
685 let result = compute_layout(
686 &score,
687 &LayoutConfig {
688 measures_per_row: 4,
689 concert_pitch: false,
690 first_row_measures: None,
691 },
692 );
693 assert!(result.concert_key_overrides.is_empty());
694 }
695
696 #[test]
697 fn concert_pitch_zero_transpose_no_override() {
698 let score = score_with_measures(1);
700 let result = compute_layout(
701 &score,
702 &LayoutConfig {
703 measures_per_row: 4,
704 concert_pitch: true,
705 first_row_measures: None,
706 },
707 );
708 assert!(result.concert_key_overrides.is_empty());
709 }
710
711 #[test]
712 fn transpose_key_formula_eb_alto_sax() {
713 assert_eq!(transpose_key_fifths(0, -9), -3);
716 }
717
718 #[test]
719 fn transpose_key_formula_perfect_fifth_up() {
720 assert_eq!(transpose_key_fifths(0, 7), 1);
722 }
723
724 #[test]
725 fn system_break_splits_row() {
726 let mut score = score_with_measures(3);
728 score.parts[0].staves[0].measures[1].system_break = true;
729 let result = compute_layout(
730 &score,
731 &LayoutConfig {
732 measures_per_row: 4,
733 concert_pitch: false,
734 first_row_measures: None,
735 },
736 );
737 assert_eq!(result.rows.len(), 2);
738 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
739 assert_eq!(result.rows[1].measure_indices, vec![2]);
740 }
741
742 #[test]
743 fn page_break_splits_row() {
744 let mut score = score_with_measures(3);
745 score.parts[0].staves[0].measures[0].page_break = true;
746 let result = compute_layout(
747 &score,
748 &LayoutConfig {
749 measures_per_row: 4,
750 concert_pitch: false,
751 first_row_measures: None,
752 },
753 );
754 assert_eq!(result.rows.len(), 2);
755 assert_eq!(result.rows[0].measure_indices, vec![0]);
756 assert_eq!(result.rows[1].measure_indices, vec![1, 2]);
757 }
758
759 #[test]
760 fn system_break_overrides_per_row() {
761 let mut score = score_with_measures(5);
763 score.parts[0].staves[0].measures[1].system_break = true;
764 let result = compute_layout(
765 &score,
766 &LayoutConfig {
767 measures_per_row: 4,
768 concert_pitch: false,
769 first_row_measures: None,
770 },
771 );
772 assert_eq!(result.rows.len(), 2);
773 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
774 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
775 }
776
777 #[test]
778 fn no_break_unchanged() {
779 let score = score_with_measures(3);
780 let result = compute_layout(
781 &score,
782 &LayoutConfig {
783 measures_per_row: 4,
784 concert_pitch: false,
785 first_row_measures: None,
786 },
787 );
788 assert_eq!(result.rows.len(), 1);
789 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2]);
790 }
791
792 #[test]
795 fn first_row_measures_limits_first_row() {
796 let score = score_with_measures(5);
798 let result = compute_layout(
799 &score,
800 &LayoutConfig {
801 measures_per_row: 4,
802 concert_pitch: false,
803 first_row_measures: Some(2),
804 },
805 );
806 assert_eq!(result.rows.len(), 2);
807 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
808 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
809 }
810
811 #[test]
812 fn first_row_measures_none_unchanged() {
813 let score = score_with_measures(5);
815 let without = compute_layout(
816 &score,
817 &LayoutConfig {
818 measures_per_row: 4,
819 concert_pitch: false,
820 first_row_measures: None,
821 },
822 );
823 let with_none = compute_layout(&score, &LayoutConfig::default());
824 assert_eq!(without.rows.len(), with_none.rows.len());
825 for (a, b) in without.rows.iter().zip(with_none.rows.iter()) {
826 assert_eq!(a.measure_indices, b.measure_indices);
827 }
828 }
829
830 #[test]
831 fn beam_groups_empty_for_default_score() {
832 let score = Score::default();
833 let result = compute_layout(&score, &LayoutConfig::default());
834 assert!(result.beam_groups.is_empty());
835 }
836
837 #[test]
838 fn layout_exposes_source_voice_number_for_canonical_slot() {
839 let mut score = score_with_measures(1);
840 score.parts[0].staves[0].measures[0].source_voice_numbers[1] = Some(5);
841
842 let result = compute_layout(&score, &LayoutConfig::default());
843 assert_eq!(
844 result.source_voice_addresses,
845 vec![SourceVoiceAddress {
846 part: 0,
847 staff: 0,
848 measure: 0,
849 voice: 1,
850 source_voice_number: 5,
851 }]
852 );
853 }
854
855 #[test]
856 fn beam_groups_begin_end_collected() {
857 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
858 let mut score = score_with_measures(1);
859 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
860 n1.beam = BeamState::Begin;
861 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
862 n2.beam = BeamState::Continue;
863 let mut n3 = Note::new(Pitch::new(Step::E, 4), Duration::Eighth);
864 n3.beam = BeamState::End;
865 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2, n3];
866
867 let result = compute_layout(&score, &LayoutConfig::default());
868 assert_eq!(result.beam_groups.len(), 1);
869 assert_eq!(result.beam_groups[0].note_indices, vec![0, 1, 2]);
870 assert_eq!(result.beam_groups[0].part, 0);
871 assert_eq!(result.beam_groups[0].measure, 0);
872 assert_eq!(result.beam_groups[0].voice, 0);
873 }
874
875 #[test]
876 fn beam_groups_beginend_is_standalone_group() {
877 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
878 let mut score = score_with_measures(1);
879 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
880 n.beam = BeamState::BeginEnd;
881 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
882
883 let result = compute_layout(&score, &LayoutConfig::default());
884 assert_eq!(result.beam_groups.len(), 1);
885 assert_eq!(result.beam_groups[0].note_indices, vec![0]);
886 }
887
888 #[test]
889 fn tuplet_groups_empty_for_default_score() {
890 let score = Score::default();
891 let result = compute_layout(&score, &LayoutConfig::default());
892 assert!(result.tuplet_groups.is_empty());
893 }
894
895 #[test]
896 fn tuplet_groups_triplet_collected() {
897 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
898 let ti = TupletInfo {
899 actual_notes: 3,
900 normal_notes: 2,
901 };
902 let mut score = score_with_measures(1);
903 let notes: Vec<Note> = (0..3)
904 .map(|i| {
905 let step = [Step::C, Step::D, Step::E][i].clone();
906 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
907 n.tuplet = Some(ti.clone());
908 n
909 })
910 .collect();
911 score.parts[0].staves[0].measures[0].voices[0] = notes;
912
913 let result = compute_layout(&score, &LayoutConfig::default());
914 assert_eq!(result.tuplet_groups.len(), 1);
915 assert_eq!(result.tuplet_groups[0].note_indices, vec![0, 1, 2]);
916 assert_eq!(result.tuplet_groups[0].actual_notes, 3);
917 assert_eq!(result.tuplet_groups[0].normal_notes, 2);
918 assert_eq!(result.tuplet_groups[0].part, 0);
919 assert_eq!(result.tuplet_groups[0].measure, 0);
920 }
921
922 #[test]
923 fn tuplet_groups_no_group_for_single_tuplet_note() {
924 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
925 let mut score = score_with_measures(1);
926 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
927 n.tuplet = Some(TupletInfo {
928 actual_notes: 3,
929 normal_notes: 2,
930 });
931 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
932
933 let result = compute_layout(&score, &LayoutConfig::default());
934 assert!(result.tuplet_groups.is_empty());
936 }
937
938 #[test]
939 fn hairpin_span_resolved() {
940 use acorde_core::{Duration, HairpinKind, Note, Pitch, Step};
941 let mut score = score_with_measures(1);
942 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
943 n1.hairpin_start = Some(HairpinKind::Crescendo);
944 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
945 n2.hairpin_end = true;
946 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
947
948 let result = compute_layout(&score, &LayoutConfig::default());
949 assert_eq!(result.spans.len(), 1);
950 if let SpanMark::Hairpin { kind, start, end } = &result.spans[0] {
951 assert_eq!(*kind, HairpinKind::Crescendo);
952 assert_eq!(start.note, 0);
953 assert_eq!(end.note, 1);
954 } else {
955 panic!("expected Hairpin span");
956 }
957 }
958
959 #[test]
960 fn slur_span_resolved() {
961 use acorde_core::{Duration, Note, Pitch, Step};
962 let mut score = score_with_measures(1);
963 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
964 n1.slur_start = true;
965 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
966 n2.slur_end = true;
967 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
968
969 let result = compute_layout(&score, &LayoutConfig::default());
970 let slur = result
971 .spans
972 .iter()
973 .find(|s| matches!(s, SpanMark::Slur { .. }));
974 assert!(slur.is_some(), "expected a Slur span");
975 if let Some(SpanMark::Slur { start, end }) = slur {
976 assert_eq!(start.note, 0);
977 assert_eq!(end.note, 1);
978 }
979 }
980
981 #[test]
982 fn typed_spanners_keep_stable_identity_in_layout_result() {
983 use acorde_core::{Duration, NotationSpanner, NotationSpannerKind, Note, Pitch, Step};
984 let mut score = score_with_measures(1);
985 score.parts[0].staves[0].measures[0].voices[0] = vec![
986 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
987 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
988 ];
989 score.spanners.push(NotationSpanner {
990 id: "typed-slur-7".to_string(),
991 kind: NotationSpannerKind::Slur,
992 start: NoteAddr {
993 part: 0,
994 staff: 0,
995 measure: 0,
996 voice: 0,
997 note: 0,
998 },
999 end: NoteAddr {
1000 part: 0,
1001 staff: 0,
1002 measure: 0,
1003 voice: 0,
1004 note: 1,
1005 },
1006 number: Some(7),
1007 line_type: Some("dashed".to_string()),
1008 text: None,
1009 placement: Some("above".to_string()),
1010 ottava_size: None,
1011 ottava_type: None,
1012 });
1013
1014 let result = compute_layout(&score, &LayoutConfig::default());
1015 assert_eq!(result.typed_spanners.len(), 1);
1016 assert_eq!(result.typed_spanners[0].id, "typed-slur-7");
1017 assert_eq!(result.typed_spanners[0].number, Some(7));
1018 }
1019
1020 #[test]
1021 fn harmony_range_span_resolved_from_typed_end_address() {
1022 use acorde_core::{ChordSymbol, Duration, Note, NoteAddr, Pitch, Step};
1023 let mut score = score_with_measures(2);
1024 score.parts[0].staves[0].measures[0].voices[0] =
1025 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
1026 score.parts[0].staves[0].measures[1].voices[0] =
1027 vec![Note::new(Pitch::new(Step::G, 4), Duration::Whole)];
1028 score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
1029 root: "C".to_owned(),
1030 kind: "major".to_owned(),
1031 bass: None,
1032 placement: None,
1033 extender: true,
1034 harmonic_degree: None,
1035 harmony_function: None,
1036 harmony_type: None,
1037 chord_ref: None,
1038 range_end: Some(NoteAddr {
1039 part: 0,
1040 staff: 0,
1041 measure: 1,
1042 voice: 0,
1043 note: 0,
1044 }),
1045 degrees: Vec::new(),
1046 });
1047
1048 let result = compute_layout(&score, &LayoutConfig::default());
1049 assert!(matches!(
1050 result.spans.as_slice(),
1051 [SpanMark::Harmony { label, start, end }]
1052 if label == "C" && start.measure == 0 && end.measure == 1
1053 ));
1054 }
1055
1056 #[test]
1059 fn courtesy_none_for_clean_score() {
1060 let score = Score::default();
1062 let result = compute_layout(&score, &LayoutConfig::default());
1063 assert!(result.courtesy_accidentals.is_empty());
1064 }
1065
1066 #[test]
1067 fn courtesy_after_chromatic_alteration() {
1068 use acorde_core::{Duration, Note, Pitch, Step};
1071 let mut score = score_with_measures(2);
1072
1073 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1074 fsharp.pitches[0].alter = 1;
1075 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1076
1077 let fnat = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1078 score.parts[0].staves[0].measures[1].voices[0] = vec![fnat];
1080
1081 let result = compute_layout(&score, &LayoutConfig::default());
1082 assert_eq!(result.courtesy_accidentals.len(), 1);
1083 let ca = &result.courtesy_accidentals[0];
1084 assert_eq!(ca.measure, 1);
1085 assert_eq!(ca.alter, 0); }
1087
1088 #[test]
1089 fn courtesy_same_alteration_no_courtesy() {
1090 use acorde_core::{Duration, Note, Pitch, Step};
1096 let mut score = score_with_measures(2);
1097
1098 let mut fsharp1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1099 fsharp1.pitches[0].alter = 1;
1100 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp1];
1101
1102 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1103 fsharp2.pitches[0].alter = 1;
1104 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
1105
1106 let result = compute_layout(&score, &LayoutConfig::default());
1107 assert_eq!(result.courtesy_accidentals.len(), 1);
1109 assert_eq!(result.courtesy_accidentals[0].alter, 1);
1110 }
1111
1112 #[test]
1113 fn courtesy_tied_note_excluded() {
1114 use acorde_core::{Duration, Note, Pitch, Step};
1116 let mut score = score_with_measures(2);
1117
1118 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1119 fsharp.pitches[0].alter = 1;
1120 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1121
1122 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1123 tied.pitches[0].alter = 1;
1124 tied.tie_end = true;
1125 score.parts[0].staves[0].measures[1].voices[0] = vec![tied];
1126
1127 let result = compute_layout(&score, &LayoutConfig::default());
1128 assert!(result.courtesy_accidentals.is_empty());
1129 }
1130
1131 #[test]
1132 fn courtesy_key_change_resets() {
1133 use acorde_core::{Duration, KeySignature, Note, Pitch, Step};
1136 let mut score = score_with_measures(2);
1137
1138 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1139 fsharp.pitches[0].alter = 1;
1140 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1141
1142 score.parts[0].staves[0].measures[1].key_sig = Some(KeySignature {
1144 fifths: 1,
1145 mode: "major".to_string(),
1146 });
1147 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1149 fsharp2.pitches[0].alter = 1;
1150 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
1151
1152 let result = compute_layout(&score, &LayoutConfig::default());
1153 assert!(result.courtesy_accidentals.is_empty());
1155 }
1156
1157 #[test]
1160 fn accidental_none_for_clean_score() {
1161 let score = Score::default();
1162 let result = compute_layout(&score, &LayoutConfig::default());
1163 assert!(result.accidentals.is_empty());
1164 }
1165
1166 #[test]
1167 fn accidental_first_chromatic_note_in_measure() {
1168 use acorde_core::{Duration, Note, Pitch, Step};
1170 let mut score = score_with_measures(1);
1171 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1172 fsharp.pitches[0].alter = 1;
1173 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1174
1175 let result = compute_layout(&score, &LayoutConfig::default());
1176 assert_eq!(result.accidentals.len(), 1);
1177 assert_eq!(result.accidentals[0].alter, 1);
1178 assert_eq!(result.accidentals[0].measure, 0);
1179 }
1180
1181 #[test]
1182 fn accidental_repeat_same_alter_in_measure_not_marked_again() {
1183 use acorde_core::{Duration, Note, Pitch, Step};
1185 let mut score = score_with_measures(1);
1186 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1187 f1.pitches[0].alter = 1;
1188 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1189 f2.pitches[0].alter = 1;
1190 score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
1191
1192 let result = compute_layout(&score, &LayoutConfig::default());
1193 assert_eq!(result.accidentals.len(), 1);
1194 }
1195
1196 #[test]
1197 fn accidental_reverts_to_natural_within_same_measure() {
1198 use acorde_core::{Duration, Note, Pitch, Step};
1200 let mut score = score_with_measures(1);
1201 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1202 f1.pitches[0].alter = 1;
1203 let f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter); score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
1205
1206 let result = compute_layout(&score, &LayoutConfig::default());
1207 assert_eq!(result.accidentals.len(), 2);
1208 assert_eq!(result.accidentals[0].alter, 1);
1209 assert_eq!(result.accidentals[1].alter, 0);
1210 }
1211
1212 #[test]
1213 fn accidental_does_not_carry_across_barline() {
1214 use acorde_core::{Duration, Note, Pitch, Step};
1218 let mut score = score_with_measures(2);
1219 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1220 f1.pitches[0].alter = 1;
1221 score.parts[0].staves[0].measures[0].voices[0] = vec![f1];
1222 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1223 f2.pitches[0].alter = 1;
1224 score.parts[0].staves[0].measures[1].voices[0] = vec![f2];
1225
1226 let result = compute_layout(&score, &LayoutConfig::default());
1227 assert_eq!(result.accidentals.len(), 2);
1228 assert!(result.accidentals.iter().any(|a| a.measure == 1));
1229 assert_eq!(result.courtesy_accidentals.len(), 1);
1231 assert_eq!(result.courtesy_accidentals[0].measure, 1);
1232 }
1233
1234 #[test]
1235 fn accidental_tied_note_excluded() {
1236 use acorde_core::{Duration, Note, Pitch, Step};
1237 let mut score = score_with_measures(1);
1238 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1239 tied.pitches[0].alter = 1;
1240 tied.tie_end = true;
1241 score.parts[0].staves[0].measures[0].voices[0] = vec![tied];
1242
1243 let result = compute_layout(&score, &LayoutConfig::default());
1244 assert!(result.accidentals.is_empty());
1245 }
1246
1247 #[test]
1248 fn accidental_shared_across_voices_on_same_staff() {
1249 use acorde_core::{Duration, Note, Pitch, Step};
1251 let mut score = score_with_measures(1);
1252 let mut f_v0 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1253 f_v0.pitches[0].alter = 1;
1254 score.parts[0].staves[0].measures[0].voices[0] = vec![f_v0];
1255 let mut f_v1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1256 f_v1.pitches[0].alter = 1;
1257 score.parts[0].staves[0].measures[0].voices[1] = vec![f_v1];
1258
1259 let result = compute_layout(&score, &LayoutConfig::default());
1260 assert_eq!(result.accidentals.len(), 1);
1261 }
1262
1263 #[test]
1264 fn accidental_double_sharp_marked() {
1265 use acorde_core::{Duration, Note, Pitch, Step};
1266 let mut score = score_with_measures(1);
1267 let mut n = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1268 n.pitches[0].alter = 2;
1269 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
1270
1271 let result = compute_layout(&score, &LayoutConfig::default());
1272 assert_eq!(result.accidentals.len(), 1);
1273 assert_eq!(result.accidentals[0].alter, 2);
1274 }
1275}