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