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 let mut open_glissando: Option<NoteAddr> = None;
164
165 for (mi, measure) in staff.measures.iter().enumerate() {
166 for (ni, note) in measure.voices[voice].iter().enumerate() {
167 let addr = NoteAddr {
168 part: pi,
169 staff: si,
170 measure: mi,
171 voice,
172 note: ni,
173 };
174
175 if let Some(kind) = note.hairpin_start {
176 open_hairpin = Some((addr.clone(), kind));
177 }
178 if note.hairpin_end
179 && let Some((start, kind)) = open_hairpin.take()
180 {
181 spans.push(SpanMark::Hairpin {
182 kind,
183 start,
184 end: addr.clone(),
185 });
186 }
187
188 if let Some(kind) = note.ottava_start {
189 open_ottava = Some((addr.clone(), kind));
190 }
191 if note.ottava_end
192 && let Some((start, kind)) = open_ottava.take()
193 {
194 spans.push(SpanMark::Ottava {
195 kind,
196 start,
197 end: addr.clone(),
198 });
199 }
200
201 if note.pedal_start {
202 open_pedal = Some(addr.clone());
203 }
204 if note.pedal_end
205 && let Some(start) = open_pedal.take()
206 {
207 spans.push(SpanMark::Pedal {
208 start,
209 end: addr.clone(),
210 });
211 }
212
213 if note.slur_start {
214 open_slur = Some(addr.clone());
215 }
216 if note.slur_end
217 && let Some(start) = open_slur.take()
218 {
219 spans.push(SpanMark::Slur {
220 start,
221 end: addr.clone(),
222 });
223 }
224
225 if note.trill_line_start {
226 open_trill_line = Some(addr.clone());
227 }
228 if note.trill_line_end
229 && let Some(start) = open_trill_line.take()
230 {
231 spans.push(SpanMark::TrillLine {
232 start,
233 end: addr.clone(),
234 });
235 }
236
237 if note.glissando_start {
238 open_glissando = Some(addr.clone());
239 }
240 if note.glissando_end
241 && let Some(start) = open_glissando.take()
242 {
243 spans.push(SpanMark::Glissando {
244 start,
245 end: addr.clone(),
246 });
247 }
248 }
249 }
250 }
251 }
252 }
253 spans
254}
255
256fn collect_beam_groups(score: &Score) -> Vec<BeamGroup> {
259 let mut groups = Vec::new();
260 for (pi, part) in score.parts.iter().enumerate() {
261 for (si, staff) in part.staves.iter().enumerate() {
262 for (mi, measure) in staff.measures.iter().enumerate() {
263 for vi in 0..4usize {
264 let voice = &measure.voices[vi];
265 let mut current: Option<Vec<usize>> = None;
266
267 for (ni, note) in voice.iter().enumerate() {
268 match note.beam {
269 BeamState::Begin => {
270 current = Some(vec![ni]);
271 }
272 BeamState::BeginEnd => {
273 groups.push(BeamGroup {
274 part: pi,
275 staff: si,
276 measure: mi,
277 voice: vi,
278 note_indices: vec![ni],
279 });
280 }
281 BeamState::Continue
282 | BeamState::ForwardHook
283 | BeamState::BackwardHook => {
284 if let Some(ref mut g) = current {
285 g.push(ni);
286 }
287 }
288 BeamState::End => {
289 if let Some(mut g) = current.take() {
290 g.push(ni);
291 groups.push(BeamGroup {
292 part: pi,
293 staff: si,
294 measure: mi,
295 voice: vi,
296 note_indices: g,
297 });
298 }
299 }
300 BeamState::None => {
301 current = None;
302 }
303 }
304 }
305 }
306 }
307 }
308 }
309 groups
310}
311
312fn collect_tuplet_groups(score: &Score) -> Vec<TupletGroup> {
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_indices: Vec<usize> = Vec::new();
322 let mut current_info: Option<TupletInfo> = None;
323
324 let flush = |indices: &mut Vec<usize>,
325 info: &mut Option<TupletInfo>,
326 groups: &mut Vec<TupletGroup>| {
327 if indices.len() >= 2
328 && let Some(ti) = info.take()
329 {
330 groups.push(TupletGroup {
331 part: pi,
332 staff: si,
333 measure: mi,
334 voice: vi,
335 note_indices: std::mem::take(indices),
336 actual_notes: ti.actual_notes,
337 normal_notes: ti.normal_notes,
338 });
339 return;
340 }
341 indices.clear();
342 info.take();
343 };
344
345 for (ni, note) in voice.iter().enumerate() {
346 match ¬e.tuplet {
347 Some(ti) => {
348 let same_group = current_info.as_ref().is_some_and(|prev| {
349 prev.actual_notes == ti.actual_notes
350 && prev.normal_notes == ti.normal_notes
351 });
352 if !same_group {
353 flush(&mut current_indices, &mut current_info, &mut groups);
354 current_info = Some(ti.clone());
355 }
356 current_indices.push(ni);
357 }
358 None => {
359 flush(&mut current_indices, &mut current_info, &mut groups);
360 }
361 }
362 }
363 flush(&mut current_indices, &mut current_info, &mut groups);
364 }
365 }
366 }
367 }
368 groups
369}
370
371fn step_idx(step: &Step) -> u8 {
372 match step {
373 Step::C => 0,
374 Step::D => 1,
375 Step::E => 2,
376 Step::F => 3,
377 Step::G => 4,
378 Step::A => 5,
379 Step::B => 6,
380 }
381}
382
383fn key_alter(fifths: i8, step: &Step) -> i8 {
385 const SHARPS: [Step; 7] = [
386 Step::F,
387 Step::C,
388 Step::G,
389 Step::D,
390 Step::A,
391 Step::E,
392 Step::B,
393 ];
394 const FLATS: [Step; 7] = [
395 Step::B,
396 Step::E,
397 Step::A,
398 Step::D,
399 Step::G,
400 Step::C,
401 Step::F,
402 ];
403 if fifths > 0 && SHARPS[..fifths as usize].contains(step) {
404 1
405 } else if fifths < 0 && FLATS[..(-fifths) as usize].contains(step) {
406 -1
407 } else {
408 0
409 }
410}
411
412fn collect_courtesy_accidentals(score: &Score) -> Vec<CourtesyAccidental> {
413 let mut result = Vec::new();
414 let base_fifths = score.settings.key_signature.fifths;
415
416 for (pi, part) in score.parts.iter().enumerate() {
417 for (si, staff) in part.staves.iter().enumerate() {
418 let mut prev_alters: HashMap<(u8, i8), i8> = HashMap::new();
420 let mut current_fifths = base_fifths;
421
422 for (mi, measure) in staff.measures.iter().enumerate() {
423 if let Some(key_sig) = measure.key_sig.as_ref() {
424 current_fifths = key_sig.fifths;
425 prev_alters.clear();
426 }
427
428 for vi in 0..4usize {
430 for (ni, note) in measure.voices[vi].iter().enumerate() {
431 if note.tie_end {
432 continue;
433 }
434 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
435 let key = (step_idx(&pitch.step), pitch.octave);
436 if prev_alters.contains_key(&key) {
437 result.push(CourtesyAccidental {
438 part: pi,
439 staff: si,
440 measure: mi,
441 voice: vi,
442 note_index: ni,
443 pitch_index: pitch_idx,
444 alter: pitch.alter,
445 });
446 }
447 }
448 }
449 }
450
451 prev_alters.clear();
453 for vi in 0..4usize {
454 for note in measure.voices[vi].iter() {
455 for pitch in note.pitches.iter() {
456 if pitch.alter != key_alter(current_fifths, &pitch.step) {
457 prev_alters
458 .insert((step_idx(&pitch.step), pitch.octave), pitch.alter);
459 }
460 }
461 }
462 }
463 }
464 }
465 }
466 result
467}
468
469fn collect_accidental_marks(score: &Score) -> Vec<AccidentalMark> {
478 let mut result = Vec::new();
479 let base_fifths = score.settings.key_signature.fifths;
480
481 for (pi, part) in score.parts.iter().enumerate() {
482 for (si, staff) in part.staves.iter().enumerate() {
483 let mut current_fifths = base_fifths;
484
485 for (mi, measure) in staff.measures.iter().enumerate() {
486 if let Some(key_sig) = measure.key_sig.as_ref() {
487 current_fifths = key_sig.fifths;
488 }
489
490 let mut active: HashMap<(u8, i8), i8> = HashMap::new();
492
493 for vi in 0..4usize {
494 for (ni, note) in measure.voices[vi].iter().enumerate() {
495 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
496 let key = (step_idx(&pitch.step), pitch.octave);
497 let baseline = active
498 .get(&key)
499 .copied()
500 .unwrap_or_else(|| key_alter(current_fifths, &pitch.step));
501 if pitch.alter != baseline {
502 if !note.tie_end {
503 result.push(AccidentalMark {
504 part: pi,
505 staff: si,
506 measure: mi,
507 voice: vi,
508 note_index: ni,
509 pitch_index: pitch_idx,
510 alter: pitch.alter,
511 });
512 }
513 active.insert(key, pitch.alter);
514 }
515 }
516 }
517 }
518 }
519 }
520 }
521 result
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use acorde_core::{Clef, Measure, Part, Score, Staff};
528
529 fn score_with_measures(n: usize) -> Score {
530 let mut score = Score::default();
531 score.parts.clear();
532 let mut staff = Staff::new(Clef::Treble);
533 for i in 0..n {
534 let mut m = Measure::empty(4, 4);
535 m.number = i as u32 + 1;
536 staff.measures.push(m);
537 }
538 let mut part = Part::new("P1", "P1");
539 part.staves.push(staff);
540 score.parts.push(part);
541 score
542 }
543
544 #[test]
545 fn default_score_has_one_row_of_four() {
546 let score = Score::default();
548 let result = compute_layout(&score, &LayoutConfig::default());
549 assert_eq!(result.rows.len(), 1);
550 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
551 assert_eq!(result.vis_slots.len(), 4);
552 assert!(result.spans.is_empty());
553 }
554
555 #[test]
556 fn vis_slots_expand_multi_rest() {
557 let mut score = score_with_measures(1);
558 score.parts[0].staves[0].measures[0].multi_rest_count = Some(4);
559
560 let result = compute_layout(
561 &score,
562 &LayoutConfig {
563 measures_per_row: 8,
564 concert_pitch: false,
565 first_row_measures: None,
566 },
567 );
568 assert_eq!(result.vis_slots.len(), 4);
569 assert!(result.vis_slots.iter().all(|&v| v == 0));
570 }
571
572 #[test]
573 fn measures_split_into_rows() {
574 let score = score_with_measures(6);
575 let result = compute_layout(
576 &score,
577 &LayoutConfig {
578 measures_per_row: 4,
579 concert_pitch: false,
580 first_row_measures: None,
581 },
582 );
583 assert_eq!(result.rows.len(), 2);
584 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
585 assert_eq!(result.rows[1].measure_indices, vec![4, 5]);
586 }
587
588 #[test]
589 fn single_measure_single_row() {
590 let score = score_with_measures(1);
591 let result = compute_layout(
592 &score,
593 &LayoutConfig {
594 measures_per_row: 4,
595 concert_pitch: false,
596 first_row_measures: None,
597 },
598 );
599 assert_eq!(result.rows.len(), 1);
600 assert_eq!(result.rows[0].measure_indices, vec![0]);
601 }
602
603 #[test]
604 fn exactly_full_rows() {
605 let score = score_with_measures(8);
606 let result = compute_layout(
607 &score,
608 &LayoutConfig {
609 measures_per_row: 4,
610 concert_pitch: false,
611 first_row_measures: None,
612 },
613 );
614 assert_eq!(result.rows.len(), 2);
615 assert_eq!(result.rows[0].measure_indices.len(), 4);
616 assert_eq!(result.rows[1].measure_indices.len(), 4);
617 }
618
619 #[test]
620 fn concert_pitch_bb_clarinet_transposes_key() {
621 let mut score = score_with_measures(1);
624 score.parts[0].staves[0].transpose_semitones = -2;
625 let result = compute_layout(
626 &score,
627 &LayoutConfig {
628 measures_per_row: 4,
629 concert_pitch: true,
630 first_row_measures: None,
631 },
632 );
633 assert_eq!(result.concert_key_overrides.len(), 1);
634 assert_eq!(result.concert_key_overrides[0].part_index, 0);
635 assert_eq!(result.concert_key_overrides[0].staff_index, 0);
636 assert_eq!(result.concert_key_overrides[0].fifths, -2);
637 }
638
639 #[test]
640 fn concert_pitch_false_no_override() {
641 let mut score = score_with_measures(1);
642 score.parts[0].staves[0].transpose_semitones = -2;
643 let result = compute_layout(
644 &score,
645 &LayoutConfig {
646 measures_per_row: 4,
647 concert_pitch: false,
648 first_row_measures: None,
649 },
650 );
651 assert!(result.concert_key_overrides.is_empty());
652 }
653
654 #[test]
655 fn concert_pitch_zero_transpose_no_override() {
656 let score = score_with_measures(1);
658 let result = compute_layout(
659 &score,
660 &LayoutConfig {
661 measures_per_row: 4,
662 concert_pitch: true,
663 first_row_measures: None,
664 },
665 );
666 assert!(result.concert_key_overrides.is_empty());
667 }
668
669 #[test]
670 fn transpose_key_formula_eb_alto_sax() {
671 assert_eq!(transpose_key_fifths(0, -9), -3);
674 }
675
676 #[test]
677 fn transpose_key_formula_perfect_fifth_up() {
678 assert_eq!(transpose_key_fifths(0, 7), 1);
680 }
681
682 #[test]
683 fn system_break_splits_row() {
684 let mut score = score_with_measures(3);
686 score.parts[0].staves[0].measures[1].system_break = true;
687 let result = compute_layout(
688 &score,
689 &LayoutConfig {
690 measures_per_row: 4,
691 concert_pitch: false,
692 first_row_measures: None,
693 },
694 );
695 assert_eq!(result.rows.len(), 2);
696 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
697 assert_eq!(result.rows[1].measure_indices, vec![2]);
698 }
699
700 #[test]
701 fn page_break_splits_row() {
702 let mut score = score_with_measures(3);
703 score.parts[0].staves[0].measures[0].page_break = true;
704 let result = compute_layout(
705 &score,
706 &LayoutConfig {
707 measures_per_row: 4,
708 concert_pitch: false,
709 first_row_measures: None,
710 },
711 );
712 assert_eq!(result.rows.len(), 2);
713 assert_eq!(result.rows[0].measure_indices, vec![0]);
714 assert_eq!(result.rows[1].measure_indices, vec![1, 2]);
715 }
716
717 #[test]
718 fn system_break_overrides_per_row() {
719 let mut score = score_with_measures(5);
721 score.parts[0].staves[0].measures[1].system_break = true;
722 let result = compute_layout(
723 &score,
724 &LayoutConfig {
725 measures_per_row: 4,
726 concert_pitch: false,
727 first_row_measures: None,
728 },
729 );
730 assert_eq!(result.rows.len(), 2);
731 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
732 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
733 }
734
735 #[test]
736 fn no_break_unchanged() {
737 let score = score_with_measures(3);
738 let result = compute_layout(
739 &score,
740 &LayoutConfig {
741 measures_per_row: 4,
742 concert_pitch: false,
743 first_row_measures: None,
744 },
745 );
746 assert_eq!(result.rows.len(), 1);
747 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2]);
748 }
749
750 #[test]
753 fn first_row_measures_limits_first_row() {
754 let score = score_with_measures(5);
756 let result = compute_layout(
757 &score,
758 &LayoutConfig {
759 measures_per_row: 4,
760 concert_pitch: false,
761 first_row_measures: Some(2),
762 },
763 );
764 assert_eq!(result.rows.len(), 2);
765 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
766 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
767 }
768
769 #[test]
770 fn first_row_measures_none_unchanged() {
771 let score = score_with_measures(5);
773 let without = compute_layout(
774 &score,
775 &LayoutConfig {
776 measures_per_row: 4,
777 concert_pitch: false,
778 first_row_measures: None,
779 },
780 );
781 let with_none = compute_layout(&score, &LayoutConfig::default());
782 assert_eq!(without.rows.len(), with_none.rows.len());
783 for (a, b) in without.rows.iter().zip(with_none.rows.iter()) {
784 assert_eq!(a.measure_indices, b.measure_indices);
785 }
786 }
787
788 #[test]
789 fn beam_groups_empty_for_default_score() {
790 let score = Score::default();
791 let result = compute_layout(&score, &LayoutConfig::default());
792 assert!(result.beam_groups.is_empty());
793 }
794
795 #[test]
796 fn beam_groups_begin_end_collected() {
797 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
798 let mut score = score_with_measures(1);
799 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
800 n1.beam = BeamState::Begin;
801 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
802 n2.beam = BeamState::Continue;
803 let mut n3 = Note::new(Pitch::new(Step::E, 4), Duration::Eighth);
804 n3.beam = BeamState::End;
805 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2, n3];
806
807 let result = compute_layout(&score, &LayoutConfig::default());
808 assert_eq!(result.beam_groups.len(), 1);
809 assert_eq!(result.beam_groups[0].note_indices, vec![0, 1, 2]);
810 assert_eq!(result.beam_groups[0].part, 0);
811 assert_eq!(result.beam_groups[0].measure, 0);
812 assert_eq!(result.beam_groups[0].voice, 0);
813 }
814
815 #[test]
816 fn beam_groups_beginend_is_standalone_group() {
817 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
818 let mut score = score_with_measures(1);
819 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
820 n.beam = BeamState::BeginEnd;
821 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
822
823 let result = compute_layout(&score, &LayoutConfig::default());
824 assert_eq!(result.beam_groups.len(), 1);
825 assert_eq!(result.beam_groups[0].note_indices, vec![0]);
826 }
827
828 #[test]
829 fn tuplet_groups_empty_for_default_score() {
830 let score = Score::default();
831 let result = compute_layout(&score, &LayoutConfig::default());
832 assert!(result.tuplet_groups.is_empty());
833 }
834
835 #[test]
836 fn tuplet_groups_triplet_collected() {
837 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
838 let ti = TupletInfo {
839 actual_notes: 3,
840 normal_notes: 2,
841 };
842 let mut score = score_with_measures(1);
843 let notes: Vec<Note> = (0..3)
844 .map(|i| {
845 let step = [Step::C, Step::D, Step::E][i].clone();
846 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
847 n.tuplet = Some(ti.clone());
848 n
849 })
850 .collect();
851 score.parts[0].staves[0].measures[0].voices[0] = notes;
852
853 let result = compute_layout(&score, &LayoutConfig::default());
854 assert_eq!(result.tuplet_groups.len(), 1);
855 assert_eq!(result.tuplet_groups[0].note_indices, vec![0, 1, 2]);
856 assert_eq!(result.tuplet_groups[0].actual_notes, 3);
857 assert_eq!(result.tuplet_groups[0].normal_notes, 2);
858 assert_eq!(result.tuplet_groups[0].part, 0);
859 assert_eq!(result.tuplet_groups[0].measure, 0);
860 }
861
862 #[test]
863 fn tuplet_groups_no_group_for_single_tuplet_note() {
864 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
865 let mut score = score_with_measures(1);
866 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
867 n.tuplet = Some(TupletInfo {
868 actual_notes: 3,
869 normal_notes: 2,
870 });
871 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
872
873 let result = compute_layout(&score, &LayoutConfig::default());
874 assert!(result.tuplet_groups.is_empty());
876 }
877
878 #[test]
879 fn hairpin_span_resolved() {
880 use acorde_core::{Duration, HairpinKind, Note, Pitch, Step};
881 let mut score = score_with_measures(1);
882 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
883 n1.hairpin_start = Some(HairpinKind::Crescendo);
884 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
885 n2.hairpin_end = true;
886 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
887
888 let result = compute_layout(&score, &LayoutConfig::default());
889 assert_eq!(result.spans.len(), 1);
890 if let SpanMark::Hairpin { kind, start, end } = &result.spans[0] {
891 assert_eq!(*kind, HairpinKind::Crescendo);
892 assert_eq!(start.note, 0);
893 assert_eq!(end.note, 1);
894 } else {
895 panic!("expected Hairpin span");
896 }
897 }
898
899 #[test]
900 fn slur_span_resolved() {
901 use acorde_core::{Duration, Note, Pitch, Step};
902 let mut score = score_with_measures(1);
903 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
904 n1.slur_start = true;
905 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
906 n2.slur_end = true;
907 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
908
909 let result = compute_layout(&score, &LayoutConfig::default());
910 let slur = result
911 .spans
912 .iter()
913 .find(|s| matches!(s, SpanMark::Slur { .. }));
914 assert!(slur.is_some(), "expected a Slur span");
915 if let Some(SpanMark::Slur { start, end }) = slur {
916 assert_eq!(start.note, 0);
917 assert_eq!(end.note, 1);
918 }
919 }
920
921 #[test]
924 fn courtesy_none_for_clean_score() {
925 let score = Score::default();
927 let result = compute_layout(&score, &LayoutConfig::default());
928 assert!(result.courtesy_accidentals.is_empty());
929 }
930
931 #[test]
932 fn courtesy_after_chromatic_alteration() {
933 use acorde_core::{Duration, Note, Pitch, Step};
936 let mut score = score_with_measures(2);
937
938 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
939 fsharp.pitches[0].alter = 1;
940 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
941
942 let fnat = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
943 score.parts[0].staves[0].measures[1].voices[0] = vec![fnat];
945
946 let result = compute_layout(&score, &LayoutConfig::default());
947 assert_eq!(result.courtesy_accidentals.len(), 1);
948 let ca = &result.courtesy_accidentals[0];
949 assert_eq!(ca.measure, 1);
950 assert_eq!(ca.alter, 0); }
952
953 #[test]
954 fn courtesy_same_alteration_no_courtesy() {
955 use acorde_core::{Duration, Note, Pitch, Step};
961 let mut score = score_with_measures(2);
962
963 let mut fsharp1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
964 fsharp1.pitches[0].alter = 1;
965 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp1];
966
967 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
968 fsharp2.pitches[0].alter = 1;
969 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
970
971 let result = compute_layout(&score, &LayoutConfig::default());
972 assert_eq!(result.courtesy_accidentals.len(), 1);
974 assert_eq!(result.courtesy_accidentals[0].alter, 1);
975 }
976
977 #[test]
978 fn courtesy_tied_note_excluded() {
979 use acorde_core::{Duration, Note, Pitch, Step};
981 let mut score = score_with_measures(2);
982
983 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
984 fsharp.pitches[0].alter = 1;
985 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
986
987 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
988 tied.pitches[0].alter = 1;
989 tied.tie_end = true;
990 score.parts[0].staves[0].measures[1].voices[0] = vec![tied];
991
992 let result = compute_layout(&score, &LayoutConfig::default());
993 assert!(result.courtesy_accidentals.is_empty());
994 }
995
996 #[test]
997 fn courtesy_key_change_resets() {
998 use acorde_core::{Duration, KeySignature, Note, Pitch, Step};
1001 let mut score = score_with_measures(2);
1002
1003 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1004 fsharp.pitches[0].alter = 1;
1005 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1006
1007 score.parts[0].staves[0].measures[1].key_sig = Some(KeySignature {
1009 fifths: 1,
1010 mode: "major".to_string(),
1011 });
1012 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1014 fsharp2.pitches[0].alter = 1;
1015 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
1016
1017 let result = compute_layout(&score, &LayoutConfig::default());
1018 assert!(result.courtesy_accidentals.is_empty());
1020 }
1021
1022 #[test]
1025 fn accidental_none_for_clean_score() {
1026 let score = Score::default();
1027 let result = compute_layout(&score, &LayoutConfig::default());
1028 assert!(result.accidentals.is_empty());
1029 }
1030
1031 #[test]
1032 fn accidental_first_chromatic_note_in_measure() {
1033 use acorde_core::{Duration, Note, Pitch, Step};
1035 let mut score = score_with_measures(1);
1036 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1037 fsharp.pitches[0].alter = 1;
1038 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1039
1040 let result = compute_layout(&score, &LayoutConfig::default());
1041 assert_eq!(result.accidentals.len(), 1);
1042 assert_eq!(result.accidentals[0].alter, 1);
1043 assert_eq!(result.accidentals[0].measure, 0);
1044 }
1045
1046 #[test]
1047 fn accidental_repeat_same_alter_in_measure_not_marked_again() {
1048 use acorde_core::{Duration, Note, Pitch, Step};
1050 let mut score = score_with_measures(1);
1051 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1052 f1.pitches[0].alter = 1;
1053 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1054 f2.pitches[0].alter = 1;
1055 score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
1056
1057 let result = compute_layout(&score, &LayoutConfig::default());
1058 assert_eq!(result.accidentals.len(), 1);
1059 }
1060
1061 #[test]
1062 fn accidental_reverts_to_natural_within_same_measure() {
1063 use acorde_core::{Duration, Note, Pitch, Step};
1065 let mut score = score_with_measures(1);
1066 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1067 f1.pitches[0].alter = 1;
1068 let f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter); score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
1070
1071 let result = compute_layout(&score, &LayoutConfig::default());
1072 assert_eq!(result.accidentals.len(), 2);
1073 assert_eq!(result.accidentals[0].alter, 1);
1074 assert_eq!(result.accidentals[1].alter, 0);
1075 }
1076
1077 #[test]
1078 fn accidental_does_not_carry_across_barline() {
1079 use acorde_core::{Duration, Note, Pitch, Step};
1083 let mut score = score_with_measures(2);
1084 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1085 f1.pitches[0].alter = 1;
1086 score.parts[0].staves[0].measures[0].voices[0] = vec![f1];
1087 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1088 f2.pitches[0].alter = 1;
1089 score.parts[0].staves[0].measures[1].voices[0] = vec![f2];
1090
1091 let result = compute_layout(&score, &LayoutConfig::default());
1092 assert_eq!(result.accidentals.len(), 2);
1093 assert!(result.accidentals.iter().any(|a| a.measure == 1));
1094 assert_eq!(result.courtesy_accidentals.len(), 1);
1096 assert_eq!(result.courtesy_accidentals[0].measure, 1);
1097 }
1098
1099 #[test]
1100 fn accidental_tied_note_excluded() {
1101 use acorde_core::{Duration, Note, Pitch, Step};
1102 let mut score = score_with_measures(1);
1103 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1104 tied.pitches[0].alter = 1;
1105 tied.tie_end = true;
1106 score.parts[0].staves[0].measures[0].voices[0] = vec![tied];
1107
1108 let result = compute_layout(&score, &LayoutConfig::default());
1109 assert!(result.accidentals.is_empty());
1110 }
1111
1112 #[test]
1113 fn accidental_shared_across_voices_on_same_staff() {
1114 use acorde_core::{Duration, Note, Pitch, Step};
1116 let mut score = score_with_measures(1);
1117 let mut f_v0 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1118 f_v0.pitches[0].alter = 1;
1119 score.parts[0].staves[0].measures[0].voices[0] = vec![f_v0];
1120 let mut f_v1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1121 f_v1.pitches[0].alter = 1;
1122 score.parts[0].staves[0].measures[0].voices[1] = vec![f_v1];
1123
1124 let result = compute_layout(&score, &LayoutConfig::default());
1125 assert_eq!(result.accidentals.len(), 1);
1126 }
1127
1128 #[test]
1129 fn accidental_double_sharp_marked() {
1130 use acorde_core::{Duration, Note, Pitch, Step};
1131 let mut score = score_with_measures(1);
1132 let mut n = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1133 n.pitches[0].alter = 2;
1134 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
1135
1136 let result = compute_layout(&score, &LayoutConfig::default());
1137 assert_eq!(result.accidentals.len(), 1);
1138 assert_eq!(result.accidentals[0].alter, 2);
1139 }
1140}