1use crate::{
2 AccidentalMark, BeamGroup, ConcertKeyOverride, CourtesyAccidental, LayoutConfig, LayoutResult,
3 RowLayout, SourceVoiceAddress, SpanMark, TupletGroup,
4};
5use acorde_core::{AccidentalDisplay, BeamState, HairpinKind, NoteAddr, OttavaKind, Score, Step};
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 for (first, last) in acorde_core::tuplet_group_ranges(voice) {
376 let note_indices: Vec<_> = (first..=last)
377 .filter(|&index| {
378 !voice[index].is_grace && voice[index].tuplet.is_some()
379 })
380 .collect();
381 if note_indices.len() >= 2
382 && let Some(ti) = voice[first].tuplet.as_ref()
383 {
384 groups.push(TupletGroup {
385 part: pi,
386 staff: si,
387 measure: mi,
388 voice: vi,
389 note_indices,
390 actual_notes: ti.actual_notes,
391 normal_notes: ti.normal_notes,
392 });
393 }
394 }
395 }
396 }
397 }
398 }
399 groups
400}
401
402fn step_idx(step: &Step) -> u8 {
403 match step {
404 Step::C => 0,
405 Step::D => 1,
406 Step::E => 2,
407 Step::F => 3,
408 Step::G => 4,
409 Step::A => 5,
410 Step::B => 6,
411 }
412}
413
414fn key_alter(fifths: i8, step: &Step) -> i8 {
416 const SHARPS: [Step; 7] = [
417 Step::F,
418 Step::C,
419 Step::G,
420 Step::D,
421 Step::A,
422 Step::E,
423 Step::B,
424 ];
425 const FLATS: [Step; 7] = [
426 Step::B,
427 Step::E,
428 Step::A,
429 Step::D,
430 Step::G,
431 Step::C,
432 Step::F,
433 ];
434 if fifths > 0 && SHARPS[..fifths as usize].contains(step) {
435 1
436 } else if fifths < 0 && FLATS[..(-fifths) as usize].contains(step) {
437 -1
438 } else {
439 0
440 }
441}
442
443fn collect_courtesy_accidentals(score: &Score) -> Vec<CourtesyAccidental> {
444 let mut result = Vec::new();
445 let base_fifths = score.settings.key_signature.fifths;
446
447 for (pi, part) in score.parts.iter().enumerate() {
448 for (si, staff) in part.staves.iter().enumerate() {
449 let mut prev_alters: HashMap<(u8, i8), i8> = HashMap::new();
451 let mut current_fifths = base_fifths;
452
453 for (mi, measure) in staff.measures.iter().enumerate() {
454 if let Some(key_sig) = measure.key_sig.as_ref() {
455 current_fifths = key_sig.fifths;
456 prev_alters.clear();
457 }
458
459 for vi in 0..4usize {
461 for (ni, note) in measure.voices[vi].iter().enumerate() {
462 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
463 let key = (step_idx(&pitch.step), pitch.octave);
464 let requested = note.accidental_display(pitch_idx)
466 == AccidentalDisplay::Parenthesized;
467 if requested || (!note.tie_end && prev_alters.contains_key(&key)) {
468 result.push(CourtesyAccidental {
469 part: pi,
470 staff: si,
471 measure: mi,
472 voice: vi,
473 note_index: ni,
474 pitch_index: pitch_idx,
475 alter: pitch.alter,
476 });
477 }
478 }
479 }
480 }
481
482 prev_alters.clear();
484 for vi in 0..4usize {
485 for note in measure.voices[vi].iter() {
486 for pitch in note.pitches.iter() {
487 if pitch.alter != key_alter(current_fifths, &pitch.step) {
488 prev_alters
489 .insert((step_idx(&pitch.step), pitch.octave), pitch.alter);
490 }
491 }
492 }
493 }
494 }
495 }
496 }
497 result
498}
499
500fn collect_accidental_marks(score: &Score) -> Vec<AccidentalMark> {
509 let mut result = Vec::new();
510 let base_fifths = score.settings.key_signature.fifths;
511
512 for (pi, part) in score.parts.iter().enumerate() {
513 for (si, staff) in part.staves.iter().enumerate() {
514 let mut current_fifths = base_fifths;
515
516 for (mi, measure) in staff.measures.iter().enumerate() {
517 if let Some(key_sig) = measure.key_sig.as_ref() {
518 current_fifths = key_sig.fifths;
519 }
520
521 let mut active: HashMap<(u8, i8), i8> = HashMap::new();
523
524 for vi in 0..4usize {
525 for (ni, note) in measure.voices[vi].iter().enumerate() {
526 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
527 let key = (step_idx(&pitch.step), pitch.octave);
528 let baseline = active
529 .get(&key)
530 .copied()
531 .unwrap_or_else(|| key_alter(current_fifths, &pitch.step));
532 let display = note.accidental_display(pitch_idx);
533 let parenthesized = display == AccidentalDisplay::Parenthesized;
534 if pitch.alter != baseline {
535 if !note.tie_end && !parenthesized {
537 result.push(AccidentalMark {
538 part: pi,
539 staff: si,
540 measure: mi,
541 voice: vi,
542 note_index: ni,
543 pitch_index: pitch_idx,
544 alter: pitch.alter,
545 });
546 }
547 active.insert(key, pitch.alter);
548 } else if matches!(
549 display,
550 AccidentalDisplay::Cautionary | AccidentalDisplay::Editorial
551 ) {
552 result.push(AccidentalMark {
555 part: pi,
556 staff: si,
557 measure: mi,
558 voice: vi,
559 note_index: ni,
560 pitch_index: pitch_idx,
561 alter: pitch.alter,
562 });
563 }
564 }
565 }
566 }
567 }
568 }
569 }
570 result
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use acorde_core::{Clef, Measure, Part, Score, Staff};
577
578 fn score_with_measures(n: usize) -> Score {
579 let mut score = Score::default();
580 score.parts.clear();
581 let mut staff = Staff::new(Clef::Treble);
582 for i in 0..n {
583 let mut m = Measure::empty(4, 4);
584 m.number = i as u32 + 1;
585 staff.measures.push(m);
586 }
587 let mut part = Part::new("P1", "P1");
588 part.staves.push(staff);
589 score.parts.push(part);
590 score
591 }
592
593 #[test]
594 fn default_score_has_one_row_of_four() {
595 let score = Score::default();
597 let result = compute_layout(&score, &LayoutConfig::default());
598 assert_eq!(result.rows.len(), 1);
599 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
600 assert_eq!(result.vis_slots.len(), 4);
601 assert!(result.spans.is_empty());
602 }
603
604 #[test]
605 fn vis_slots_expand_multi_rest() {
606 let mut score = score_with_measures(1);
607 score.parts[0].staves[0].measures[0].multi_rest_count = Some(4);
608
609 let result = compute_layout(
610 &score,
611 &LayoutConfig {
612 measures_per_row: 8,
613 concert_pitch: false,
614 first_row_measures: None,
615 },
616 );
617 assert_eq!(result.vis_slots.len(), 4);
618 assert!(result.vis_slots.iter().all(|&v| v == 0));
619 }
620
621 #[test]
622 fn measures_split_into_rows() {
623 let score = score_with_measures(6);
624 let result = compute_layout(
625 &score,
626 &LayoutConfig {
627 measures_per_row: 4,
628 concert_pitch: false,
629 first_row_measures: None,
630 },
631 );
632 assert_eq!(result.rows.len(), 2);
633 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
634 assert_eq!(result.rows[1].measure_indices, vec![4, 5]);
635 }
636
637 #[test]
638 fn single_measure_single_row() {
639 let score = score_with_measures(1);
640 let result = compute_layout(
641 &score,
642 &LayoutConfig {
643 measures_per_row: 4,
644 concert_pitch: false,
645 first_row_measures: None,
646 },
647 );
648 assert_eq!(result.rows.len(), 1);
649 assert_eq!(result.rows[0].measure_indices, vec![0]);
650 }
651
652 #[test]
653 fn exactly_full_rows() {
654 let score = score_with_measures(8);
655 let result = compute_layout(
656 &score,
657 &LayoutConfig {
658 measures_per_row: 4,
659 concert_pitch: false,
660 first_row_measures: None,
661 },
662 );
663 assert_eq!(result.rows.len(), 2);
664 assert_eq!(result.rows[0].measure_indices.len(), 4);
665 assert_eq!(result.rows[1].measure_indices.len(), 4);
666 }
667
668 #[test]
669 fn concert_pitch_bb_clarinet_transposes_key() {
670 let mut score = score_with_measures(1);
673 score.parts[0].staves[0].transpose_semitones = -2;
674 let result = compute_layout(
675 &score,
676 &LayoutConfig {
677 measures_per_row: 4,
678 concert_pitch: true,
679 first_row_measures: None,
680 },
681 );
682 assert_eq!(result.concert_key_overrides.len(), 1);
683 assert_eq!(result.concert_key_overrides[0].part_index, 0);
684 assert_eq!(result.concert_key_overrides[0].staff_index, 0);
685 assert_eq!(result.concert_key_overrides[0].fifths, -2);
686 }
687
688 #[test]
689 fn concert_pitch_false_no_override() {
690 let mut score = score_with_measures(1);
691 score.parts[0].staves[0].transpose_semitones = -2;
692 let result = compute_layout(
693 &score,
694 &LayoutConfig {
695 measures_per_row: 4,
696 concert_pitch: false,
697 first_row_measures: None,
698 },
699 );
700 assert!(result.concert_key_overrides.is_empty());
701 }
702
703 #[test]
704 fn concert_pitch_zero_transpose_no_override() {
705 let score = score_with_measures(1);
707 let result = compute_layout(
708 &score,
709 &LayoutConfig {
710 measures_per_row: 4,
711 concert_pitch: true,
712 first_row_measures: None,
713 },
714 );
715 assert!(result.concert_key_overrides.is_empty());
716 }
717
718 #[test]
719 fn transpose_key_formula_eb_alto_sax() {
720 assert_eq!(transpose_key_fifths(0, -9), -3);
723 }
724
725 #[test]
726 fn transpose_key_formula_perfect_fifth_up() {
727 assert_eq!(transpose_key_fifths(0, 7), 1);
729 }
730
731 #[test]
732 fn system_break_splits_row() {
733 let mut score = score_with_measures(3);
735 score.parts[0].staves[0].measures[1].system_break = true;
736 let result = compute_layout(
737 &score,
738 &LayoutConfig {
739 measures_per_row: 4,
740 concert_pitch: false,
741 first_row_measures: None,
742 },
743 );
744 assert_eq!(result.rows.len(), 2);
745 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
746 assert_eq!(result.rows[1].measure_indices, vec![2]);
747 }
748
749 #[test]
750 fn page_break_splits_row() {
751 let mut score = score_with_measures(3);
752 score.parts[0].staves[0].measures[0].page_break = true;
753 let result = compute_layout(
754 &score,
755 &LayoutConfig {
756 measures_per_row: 4,
757 concert_pitch: false,
758 first_row_measures: None,
759 },
760 );
761 assert_eq!(result.rows.len(), 2);
762 assert_eq!(result.rows[0].measure_indices, vec![0]);
763 assert_eq!(result.rows[1].measure_indices, vec![1, 2]);
764 }
765
766 #[test]
767 fn system_break_overrides_per_row() {
768 let mut score = score_with_measures(5);
770 score.parts[0].staves[0].measures[1].system_break = true;
771 let result = compute_layout(
772 &score,
773 &LayoutConfig {
774 measures_per_row: 4,
775 concert_pitch: false,
776 first_row_measures: None,
777 },
778 );
779 assert_eq!(result.rows.len(), 2);
780 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
781 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
782 }
783
784 #[test]
785 fn no_break_unchanged() {
786 let score = score_with_measures(3);
787 let result = compute_layout(
788 &score,
789 &LayoutConfig {
790 measures_per_row: 4,
791 concert_pitch: false,
792 first_row_measures: None,
793 },
794 );
795 assert_eq!(result.rows.len(), 1);
796 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2]);
797 }
798
799 #[test]
802 fn first_row_measures_limits_first_row() {
803 let score = score_with_measures(5);
805 let result = compute_layout(
806 &score,
807 &LayoutConfig {
808 measures_per_row: 4,
809 concert_pitch: false,
810 first_row_measures: Some(2),
811 },
812 );
813 assert_eq!(result.rows.len(), 2);
814 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
815 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
816 }
817
818 #[test]
819 fn first_row_measures_none_unchanged() {
820 let score = score_with_measures(5);
822 let without = compute_layout(
823 &score,
824 &LayoutConfig {
825 measures_per_row: 4,
826 concert_pitch: false,
827 first_row_measures: None,
828 },
829 );
830 let with_none = compute_layout(&score, &LayoutConfig::default());
831 assert_eq!(without.rows.len(), with_none.rows.len());
832 for (a, b) in without.rows.iter().zip(with_none.rows.iter()) {
833 assert_eq!(a.measure_indices, b.measure_indices);
834 }
835 }
836
837 #[test]
838 fn beam_groups_empty_for_default_score() {
839 let score = Score::default();
840 let result = compute_layout(&score, &LayoutConfig::default());
841 assert!(result.beam_groups.is_empty());
842 }
843
844 #[test]
845 fn layout_exposes_source_voice_number_for_canonical_slot() {
846 let mut score = score_with_measures(1);
847 score.parts[0].staves[0].measures[0].source_voice_numbers[1] = Some(5);
848
849 let result = compute_layout(&score, &LayoutConfig::default());
850 assert_eq!(
851 result.source_voice_addresses,
852 vec![SourceVoiceAddress {
853 part: 0,
854 staff: 0,
855 measure: 0,
856 voice: 1,
857 source_voice_number: 5,
858 }]
859 );
860 }
861
862 #[test]
863 fn beam_groups_begin_end_collected() {
864 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
865 let mut score = score_with_measures(1);
866 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
867 n1.beam = BeamState::Begin;
868 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
869 n2.beam = BeamState::Continue;
870 let mut n3 = Note::new(Pitch::new(Step::E, 4), Duration::Eighth);
871 n3.beam = BeamState::End;
872 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2, n3];
873
874 let result = compute_layout(&score, &LayoutConfig::default());
875 assert_eq!(result.beam_groups.len(), 1);
876 assert_eq!(result.beam_groups[0].note_indices, vec![0, 1, 2]);
877 assert_eq!(result.beam_groups[0].part, 0);
878 assert_eq!(result.beam_groups[0].measure, 0);
879 assert_eq!(result.beam_groups[0].voice, 0);
880 }
881
882 #[test]
883 fn beam_groups_beginend_is_standalone_group() {
884 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
885 let mut score = score_with_measures(1);
886 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
887 n.beam = BeamState::BeginEnd;
888 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
889
890 let result = compute_layout(&score, &LayoutConfig::default());
891 assert_eq!(result.beam_groups.len(), 1);
892 assert_eq!(result.beam_groups[0].note_indices, vec![0]);
893 }
894
895 #[test]
896 fn tuplet_groups_empty_for_default_score() {
897 let score = Score::default();
898 let result = compute_layout(&score, &LayoutConfig::default());
899 assert!(result.tuplet_groups.is_empty());
900 }
901
902 #[test]
903 fn tuplet_groups_triplet_collected() {
904 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
905 let ti = TupletInfo {
906 actual_notes: 3,
907 normal_notes: 2,
908 };
909 let mut score = score_with_measures(1);
910 let notes: Vec<Note> = (0..3)
911 .map(|i| {
912 let step = [Step::C, Step::D, Step::E][i].clone();
913 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
914 n.tuplet = Some(ti.clone());
915 n
916 })
917 .collect();
918 score.parts[0].staves[0].measures[0].voices[0] = notes;
919
920 let result = compute_layout(&score, &LayoutConfig::default());
921 assert_eq!(result.tuplet_groups.len(), 1);
922 assert_eq!(result.tuplet_groups[0].note_indices, vec![0, 1, 2]);
923 assert_eq!(result.tuplet_groups[0].actual_notes, 3);
924 assert_eq!(result.tuplet_groups[0].normal_notes, 2);
925 assert_eq!(result.tuplet_groups[0].part, 0);
926 assert_eq!(result.tuplet_groups[0].measure, 0);
927 }
928
929 #[test]
930 fn tuplet_groups_no_group_for_single_tuplet_note() {
931 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
932 let mut score = score_with_measures(1);
933 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
934 n.tuplet = Some(TupletInfo {
935 actual_notes: 3,
936 normal_notes: 2,
937 });
938 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
939
940 let result = compute_layout(&score, &LayoutConfig::default());
941 assert!(result.tuplet_groups.is_empty());
943 }
944
945 #[test]
946 fn hairpin_span_resolved() {
947 use acorde_core::{Duration, HairpinKind, Note, Pitch, Step};
948 let mut score = score_with_measures(1);
949 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
950 n1.hairpin_start = Some(HairpinKind::Crescendo);
951 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
952 n2.hairpin_end = true;
953 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
954
955 let result = compute_layout(&score, &LayoutConfig::default());
956 assert_eq!(result.spans.len(), 1);
957 if let SpanMark::Hairpin { kind, start, end } = &result.spans[0] {
958 assert_eq!(*kind, HairpinKind::Crescendo);
959 assert_eq!(start.note, 0);
960 assert_eq!(end.note, 1);
961 } else {
962 panic!("expected Hairpin span");
963 }
964 }
965
966 #[test]
967 fn slur_span_resolved() {
968 use acorde_core::{Duration, Note, Pitch, Step};
969 let mut score = score_with_measures(1);
970 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
971 n1.slur_start = true;
972 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
973 n2.slur_end = true;
974 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
975
976 let result = compute_layout(&score, &LayoutConfig::default());
977 let slur = result
978 .spans
979 .iter()
980 .find(|s| matches!(s, SpanMark::Slur { .. }));
981 assert!(slur.is_some(), "expected a Slur span");
982 if let Some(SpanMark::Slur { start, end }) = slur {
983 assert_eq!(start.note, 0);
984 assert_eq!(end.note, 1);
985 }
986 }
987
988 #[test]
989 fn typed_spanners_keep_stable_identity_in_layout_result() {
990 use acorde_core::{Duration, NotationSpanner, NotationSpannerKind, Note, Pitch, Step};
991 let mut score = score_with_measures(1);
992 score.parts[0].staves[0].measures[0].voices[0] = vec![
993 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
994 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
995 ];
996 score.spanners.push(NotationSpanner {
997 id: "typed-slur-7".to_string(),
998 kind: NotationSpannerKind::Slur,
999 start: NoteAddr {
1000 part: 0,
1001 staff: 0,
1002 measure: 0,
1003 voice: 0,
1004 note: 0,
1005 },
1006 end: NoteAddr {
1007 part: 0,
1008 staff: 0,
1009 measure: 0,
1010 voice: 0,
1011 note: 1,
1012 },
1013 number: Some(7),
1014 line_type: Some("dashed".to_string()),
1015 text: None,
1016 placement: Some("above".to_string()),
1017 ottava_size: None,
1018 ottava_type: None,
1019 });
1020
1021 let result = compute_layout(&score, &LayoutConfig::default());
1022 assert_eq!(result.typed_spanners.len(), 1);
1023 assert_eq!(result.typed_spanners[0].id, "typed-slur-7");
1024 assert_eq!(result.typed_spanners[0].number, Some(7));
1025 }
1026
1027 #[test]
1028 fn harmony_range_span_resolved_from_typed_end_address() {
1029 use acorde_core::{ChordSymbol, Duration, Note, NoteAddr, Pitch, Step};
1030 let mut score = score_with_measures(2);
1031 score.parts[0].staves[0].measures[0].voices[0] =
1032 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
1033 score.parts[0].staves[0].measures[1].voices[0] =
1034 vec![Note::new(Pitch::new(Step::G, 4), Duration::Whole)];
1035 score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
1036 root: "C".to_owned(),
1037 kind: "major".to_owned(),
1038 bass: None,
1039 placement: None,
1040 extender: true,
1041 harmonic_degree: None,
1042 harmony_function: None,
1043 harmony_type: None,
1044 chord_ref: None,
1045 range_end: Some(NoteAddr {
1046 part: 0,
1047 staff: 0,
1048 measure: 1,
1049 voice: 0,
1050 note: 0,
1051 }),
1052 degrees: Vec::new(),
1053 });
1054
1055 let result = compute_layout(&score, &LayoutConfig::default());
1056 assert!(matches!(
1057 result.spans.as_slice(),
1058 [SpanMark::Harmony { label, start, end }]
1059 if label == "C" && start.measure == 0 && end.measure == 1
1060 ));
1061 }
1062
1063 #[test]
1066 fn courtesy_none_for_clean_score() {
1067 let score = Score::default();
1069 let result = compute_layout(&score, &LayoutConfig::default());
1070 assert!(result.courtesy_accidentals.is_empty());
1071 }
1072
1073 #[test]
1074 fn courtesy_after_chromatic_alteration() {
1075 use acorde_core::{Duration, Note, Pitch, Step};
1078 let mut score = score_with_measures(2);
1079
1080 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1081 fsharp.pitches[0].alter = 1;
1082 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1083
1084 let fnat = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1085 score.parts[0].staves[0].measures[1].voices[0] = vec![fnat];
1087
1088 let result = compute_layout(&score, &LayoutConfig::default());
1089 assert_eq!(result.courtesy_accidentals.len(), 1);
1090 let ca = &result.courtesy_accidentals[0];
1091 assert_eq!(ca.measure, 1);
1092 assert_eq!(ca.alter, 0); }
1094
1095 #[test]
1096 fn courtesy_same_alteration_no_courtesy() {
1097 use acorde_core::{Duration, Note, Pitch, Step};
1103 let mut score = score_with_measures(2);
1104
1105 let mut fsharp1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1106 fsharp1.pitches[0].alter = 1;
1107 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp1];
1108
1109 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1110 fsharp2.pitches[0].alter = 1;
1111 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
1112
1113 let result = compute_layout(&score, &LayoutConfig::default());
1114 assert_eq!(result.courtesy_accidentals.len(), 1);
1116 assert_eq!(result.courtesy_accidentals[0].alter, 1);
1117 }
1118
1119 #[test]
1120 fn courtesy_tied_note_excluded() {
1121 use acorde_core::{Duration, Note, Pitch, Step};
1123 let mut score = score_with_measures(2);
1124
1125 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1126 fsharp.pitches[0].alter = 1;
1127 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1128
1129 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1130 tied.pitches[0].alter = 1;
1131 tied.tie_end = true;
1132 score.parts[0].staves[0].measures[1].voices[0] = vec![tied];
1133
1134 let result = compute_layout(&score, &LayoutConfig::default());
1135 assert!(result.courtesy_accidentals.is_empty());
1136 }
1137
1138 #[test]
1139 fn courtesy_key_change_resets() {
1140 use acorde_core::{Duration, KeySignature, Note, Pitch, Step};
1143 let mut score = score_with_measures(2);
1144
1145 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1146 fsharp.pitches[0].alter = 1;
1147 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1148
1149 score.parts[0].staves[0].measures[1].key_sig = Some(KeySignature {
1151 fifths: 1,
1152 mode: "major".to_string(),
1153 });
1154 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1156 fsharp2.pitches[0].alter = 1;
1157 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
1158
1159 let result = compute_layout(&score, &LayoutConfig::default());
1160 assert!(result.courtesy_accidentals.is_empty());
1162 }
1163
1164 #[test]
1167 fn requested_accidentals_are_drawn_where_the_rules_imply_them() {
1168 use acorde_core::{Duration, Note, Pitch, Step};
1169 let mut score = score_with_measures(1);
1170 let mut cautionary = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1171 cautionary.set_accidental_display(0, AccidentalDisplay::Cautionary);
1172 let mut sharp = Note::new(Pitch::with_alter(Step::F, 4, 1), Duration::Quarter);
1173 sharp.set_accidental_display(0, AccidentalDisplay::Parenthesized);
1174 let mut bracketed = Note::new(Pitch::new(Step::G, 4), Duration::Quarter);
1175 bracketed.set_accidental_display(0, AccidentalDisplay::Parenthesized);
1176 let plain = Note::new(Pitch::new(Step::A, 4), Duration::Quarter);
1177 score.parts[0].staves[0].measures[0].voices[0] = vec![cautionary, sharp, bracketed, plain];
1178
1179 let result = compute_layout(&score, &LayoutConfig::default());
1180 let mandatory: Vec<usize> = result.accidentals.iter().map(|a| a.note_index).collect();
1181 let courtesy: Vec<usize> = result
1182 .courtesy_accidentals
1183 .iter()
1184 .map(|a| a.note_index)
1185 .collect();
1186 assert_eq!(mandatory, vec![0]);
1189 assert_eq!(courtesy, vec![1, 2]);
1190 }
1191
1192 #[test]
1193 fn accidental_none_for_clean_score() {
1194 let score = Score::default();
1195 let result = compute_layout(&score, &LayoutConfig::default());
1196 assert!(result.accidentals.is_empty());
1197 }
1198
1199 #[test]
1200 fn accidental_first_chromatic_note_in_measure() {
1201 use acorde_core::{Duration, Note, Pitch, Step};
1203 let mut score = score_with_measures(1);
1204 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1205 fsharp.pitches[0].alter = 1;
1206 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1207
1208 let result = compute_layout(&score, &LayoutConfig::default());
1209 assert_eq!(result.accidentals.len(), 1);
1210 assert_eq!(result.accidentals[0].alter, 1);
1211 assert_eq!(result.accidentals[0].measure, 0);
1212 }
1213
1214 #[test]
1215 fn accidental_repeat_same_alter_in_measure_not_marked_again() {
1216 use acorde_core::{Duration, Note, Pitch, Step};
1218 let mut score = score_with_measures(1);
1219 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1220 f1.pitches[0].alter = 1;
1221 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1222 f2.pitches[0].alter = 1;
1223 score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
1224
1225 let result = compute_layout(&score, &LayoutConfig::default());
1226 assert_eq!(result.accidentals.len(), 1);
1227 }
1228
1229 #[test]
1230 fn accidental_reverts_to_natural_within_same_measure() {
1231 use acorde_core::{Duration, Note, Pitch, Step};
1233 let mut score = score_with_measures(1);
1234 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1235 f1.pitches[0].alter = 1;
1236 let f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter); score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
1238
1239 let result = compute_layout(&score, &LayoutConfig::default());
1240 assert_eq!(result.accidentals.len(), 2);
1241 assert_eq!(result.accidentals[0].alter, 1);
1242 assert_eq!(result.accidentals[1].alter, 0);
1243 }
1244
1245 #[test]
1246 fn accidental_does_not_carry_across_barline() {
1247 use acorde_core::{Duration, Note, Pitch, Step};
1251 let mut score = score_with_measures(2);
1252 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1253 f1.pitches[0].alter = 1;
1254 score.parts[0].staves[0].measures[0].voices[0] = vec![f1];
1255 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1256 f2.pitches[0].alter = 1;
1257 score.parts[0].staves[0].measures[1].voices[0] = vec![f2];
1258
1259 let result = compute_layout(&score, &LayoutConfig::default());
1260 assert_eq!(result.accidentals.len(), 2);
1261 assert!(result.accidentals.iter().any(|a| a.measure == 1));
1262 assert_eq!(result.courtesy_accidentals.len(), 1);
1264 assert_eq!(result.courtesy_accidentals[0].measure, 1);
1265 }
1266
1267 #[test]
1268 fn accidental_tied_note_excluded() {
1269 use acorde_core::{Duration, Note, Pitch, Step};
1270 let mut score = score_with_measures(1);
1271 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1272 tied.pitches[0].alter = 1;
1273 tied.tie_end = true;
1274 score.parts[0].staves[0].measures[0].voices[0] = vec![tied];
1275
1276 let result = compute_layout(&score, &LayoutConfig::default());
1277 assert!(result.accidentals.is_empty());
1278 }
1279
1280 #[test]
1281 fn accidental_shared_across_voices_on_same_staff() {
1282 use acorde_core::{Duration, Note, Pitch, Step};
1284 let mut score = score_with_measures(1);
1285 let mut f_v0 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1286 f_v0.pitches[0].alter = 1;
1287 score.parts[0].staves[0].measures[0].voices[0] = vec![f_v0];
1288 let mut f_v1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1289 f_v1.pitches[0].alter = 1;
1290 score.parts[0].staves[0].measures[0].voices[1] = vec![f_v1];
1291
1292 let result = compute_layout(&score, &LayoutConfig::default());
1293 assert_eq!(result.accidentals.len(), 1);
1294 }
1295
1296 #[test]
1297 fn accidental_double_sharp_marked() {
1298 use acorde_core::{Duration, Note, Pitch, Step};
1299 let mut score = score_with_measures(1);
1300 let mut n = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1301 n.pitches[0].alter = 2;
1302 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
1303
1304 let result = compute_layout(&score, &LayoutConfig::default());
1305 assert_eq!(result.accidentals.len(), 1);
1306 assert_eq!(result.accidentals[0].alter, 2);
1307 }
1308}