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