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