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