1use crate::{
2 AccidentalMark, BeamGroup, ConcertKeyOverride, CourtesyAccidental, LayoutConfig, LayoutResult,
3 RowLayout, SourceVoiceAddress, SpanMark, TupletGroup,
4};
5use acorde_core::{
6 AccidentalDisplay, BeamState, HairpinKind, NotationSpanner, NotationSpannerKind, NoteAddr,
7 OttavaKind, Score, Step, TupletBoundary, compute_beams,
8};
9use std::collections::{HashMap, HashSet};
10
11fn transpose_key_fifths(fifths: i8, semitones: i8) -> i8 {
16 let raw = (semitones as i16 * 7).rem_euclid(12) as i8;
17 let delta = if raw > 6 { raw - 12 } else { raw };
18 (fifths + delta).clamp(-7, 7)
19}
20
21pub fn compute_layout(score: &Score, config: &LayoutConfig) -> LayoutResult {
25 let per_row = config.measures_per_row.max(1);
26 let first_row = config.first_row_measures.map(|n| n.max(1));
27 let vis_slots = build_vis_slots(score);
28 let rows = build_rows(score, &vis_slots, per_row, first_row);
29 let spans = resolve_spans(&score.with_legacy_spanner_flags(), &score.spanners);
32
33 let mut concert_key_overrides = Vec::new();
34 if config.concert_pitch {
35 let base_fifths = score.settings.key_signature.fifths;
36 for (pi, part) in score.parts.iter().enumerate() {
37 for (si, staff) in part.staves.iter().enumerate() {
38 if staff.transpose_semitones != 0 {
39 concert_key_overrides.push(ConcertKeyOverride {
40 part_index: pi,
41 staff_index: si,
42 fifths: transpose_key_fifths(base_fifths, staff.transpose_semitones),
43 });
44 }
45 }
46 }
47 }
48
49 let beam_groups = collect_beam_groups(score, false);
50 let tuplet_groups = collect_tuplet_groups(score);
51 let courtesy_accidentals = collect_courtesy_accidentals(score);
52 let accidentals = collect_accidental_marks(score);
53 let source_voice_addresses = collect_source_voice_addresses(score);
54
55 LayoutResult {
56 vis_slots,
57 rows,
58 spans,
59 typed_spanners: score.spanners.clone(),
60 concert_key_overrides,
61 beam_groups,
62 tuplet_groups,
63 courtesy_accidentals,
64 accidentals,
65 source_voice_addresses,
66 }
67}
68
69pub fn compute_layout_with_display_beams(score: &Score, config: &LayoutConfig) -> LayoutResult {
71 let mut layout = compute_layout(score, config);
72 add_display_beams(score, &mut layout);
73 layout
74}
75
76pub fn add_display_beams(score: &Score, layout: &mut LayoutResult) {
79 layout.beam_groups = collect_beam_groups(score, true);
80}
81
82fn collect_source_voice_addresses(score: &Score) -> Vec<SourceVoiceAddress> {
83 let mut addresses = Vec::new();
84 for (part, part_value) in score.parts.iter().enumerate() {
85 for (staff, staff_value) in part_value.staves.iter().enumerate() {
86 for (measure, measure_value) in staff_value.measures.iter().enumerate() {
87 for (voice, source_voice_number) in
88 measure_value.source_voice_numbers.iter().enumerate()
89 {
90 if let Some(source_voice_number) = source_voice_number {
91 addresses.push(SourceVoiceAddress {
92 part,
93 staff,
94 measure,
95 voice,
96 source_voice_number: *source_voice_number,
97 });
98 }
99 }
100 }
101 }
102 }
103 addresses
104}
105
106fn build_vis_slots(score: &Score) -> Vec<usize> {
109 let measure_count = score
110 .parts
111 .first()
112 .and_then(|p| p.staves.first())
113 .map(|s| s.measures.len())
114 .unwrap_or(0);
115
116 let mut slots = Vec::new();
117 for phys in 0..measure_count {
118 let count = multi_rest_count(score, phys).max(1);
119 for _ in 0..count {
120 slots.push(phys);
121 }
122 }
123 slots
124}
125
126fn multi_rest_count(score: &Score, measure_index: usize) -> usize {
127 score
128 .parts
129 .iter()
130 .flat_map(|p| p.staves.iter())
131 .filter_map(|s| s.measures.get(measure_index))
132 .filter_map(|m| m.multi_rest_count)
133 .map(|c| c as usize)
134 .max()
135 .unwrap_or(1)
136}
137
138fn force_break_after(score: &Score, phys: usize) -> bool {
141 score
142 .parts
143 .iter()
144 .flat_map(|p| p.staves.iter())
145 .filter_map(|s| s.measures.get(phys))
146 .any(|m| m.system_break || m.page_break)
147}
148
149pub(crate) fn build_rows(
150 score: &Score,
151 vis_slots: &[usize],
152 per_row: usize,
153 first_row_limit: Option<usize>,
154) -> Vec<RowLayout> {
155 if vis_slots.is_empty() {
156 return vec![RowLayout {
157 measure_indices: Vec::new(),
158 }];
159 }
160
161 let mut rows: Vec<RowLayout> = Vec::new();
162 let mut current: Vec<usize> = Vec::new();
163 let mut current_width = 0usize;
164 let mut last_phys = usize::MAX;
165
166 for &phys in vis_slots {
167 if phys != last_phys {
168 let limit = if rows.is_empty() {
169 first_row_limit.unwrap_or(per_row)
170 } else {
171 per_row
172 };
173 let span_width = multi_rest_count(score, phys).max(1);
174 if !current.is_empty() && current_width.saturating_add(span_width) > limit {
175 rows.push(RowLayout {
176 measure_indices: std::mem::take(&mut current),
177 });
178 current_width = 0;
179 }
180 current.push(phys);
181 current_width = current_width.saturating_add(span_width);
182 if force_break_after(score, phys) {
183 rows.push(RowLayout {
184 measure_indices: std::mem::take(&mut current),
185 });
186 current_width = 0;
187 }
188 }
189 last_phys = phys;
190 }
191
192 if !current.is_empty() {
193 rows.push(RowLayout {
194 measure_indices: current,
195 });
196 }
197 rows
198}
199
200pub fn notation_density_units(score: &Score, measure: usize) -> usize {
205 let mut widest_staff = 3usize;
206 for staff in score.parts.iter().flat_map(|part| &part.staves) {
207 let Some(measure) = staff.measures.get(measure) else {
208 continue;
209 };
210 let voice_width = measure
211 .voices
212 .iter()
213 .map(|voice| {
214 voice
215 .iter()
216 .filter(|note| !note.hidden)
217 .map(|note| {
218 let notehead_width = if note.is_rest { 1 } else { 2 };
219 let chord_width = note.pitches.len().saturating_sub(1);
220 let accidental_width =
221 note.pitches.iter().filter(|pitch| pitch.alter != 0).count();
222 let lyric_width = note
223 .lyric
224 .as_ref()
225 .map_or(0, |lyric| lyric.text.chars().count().div_ceil(3));
226 notehead_width
227 + chord_width
228 + accidental_width
229 + usize::from(note.dot_count)
230 + lyric_width
231 + usize::from(note.chord_symbol.is_some()) * 2
232 + note.articulations.len().div_ceil(2)
233 })
234 .sum::<usize>()
235 })
236 .max()
237 .unwrap_or(0);
238 let text_width = measure
239 .texts
240 .iter()
241 .map(|text| text.text.chars().count().div_ceil(4))
242 .sum::<usize>();
243 widest_staff = widest_staff.max(voice_width + text_width + 3);
244 }
245 widest_staff
246}
247
248fn resolve_spans(score: &Score, typed_spanners: &[NotationSpanner]) -> Vec<SpanMark> {
251 let mut spans = Vec::new();
252 let typed_slur_endpoints = typed_spanners
253 .iter()
254 .filter(|span| span.kind == NotationSpannerKind::Slur)
255 .flat_map(|span| [&span.start, &span.end])
256 .map(|addr| (addr.part, addr.staff, addr.measure, addr.voice, addr.note))
257 .collect::<HashSet<_>>();
258
259 for (pi, part) in score.parts.iter().enumerate() {
260 for (si, staff) in part.staves.iter().enumerate() {
261 for voice in 0..4usize {
262 let mut open_hairpin: Option<(NoteAddr, HairpinKind)> = None;
263 let mut open_ottava: Option<(NoteAddr, OttavaKind)> = None;
264 let mut open_pedal: Option<NoteAddr> = None;
265 let mut open_slurs: Vec<NoteAddr> = Vec::new();
266 let mut open_trill_line: Option<NoteAddr> = None;
267 let mut open_glissando: Option<NoteAddr> = None;
268
269 for (mi, measure) in staff.measures.iter().enumerate() {
270 for (ni, note) in measure.voices[voice].iter().enumerate() {
271 let addr = NoteAddr {
272 part: pi,
273 staff: si,
274 measure: mi,
275 voice,
276 note: ni,
277 };
278
279 let ends_earlier = note.hairpin_end && open_hairpin.is_some();
282 if ends_earlier && let Some((start, kind)) = open_hairpin.take() {
283 spans.push(SpanMark::Hairpin {
284 kind,
285 start,
286 end: addr.clone(),
287 });
288 }
289 if let Some(kind) = note.hairpin_start {
290 open_hairpin = Some((addr.clone(), kind));
291 }
292 if note.hairpin_end
293 && !ends_earlier
294 && let Some((start, kind)) = open_hairpin.take()
295 {
296 spans.push(SpanMark::Hairpin {
297 kind,
298 start,
299 end: addr.clone(),
300 });
301 }
302
303 if let Some(kind) = note.ottava_start {
304 open_ottava = Some((addr.clone(), kind));
305 }
306 if note.ottava_end
307 && let Some((start, kind)) = open_ottava.take()
308 {
309 spans.push(SpanMark::Ottava {
310 kind,
311 start,
312 end: addr.clone(),
313 });
314 }
315
316 if note.pedal_start {
317 open_pedal = Some(addr.clone());
318 }
319 if note.pedal_end
320 && let Some(start) = open_pedal.take()
321 {
322 spans.push(SpanMark::Pedal {
323 start,
324 end: addr.clone(),
325 });
326 }
327
328 if !typed_slur_endpoints.contains(&(pi, si, mi, voice, ni)) {
332 if note.slur_end
333 && let Some(start) = open_slurs.pop()
334 {
335 spans.push(SpanMark::Slur {
336 start,
337 end: addr.clone(),
338 });
339 }
340 if note.slur_start {
341 open_slurs.push(addr.clone());
342 }
343 }
344
345 if note.trill_line_start {
346 open_trill_line = Some(addr.clone());
347 }
348 if note.trill_line_end
349 && let Some(start) = open_trill_line.take()
350 {
351 spans.push(SpanMark::TrillLine {
352 start,
353 end: addr.clone(),
354 });
355 }
356
357 if note.glissando_start {
358 open_glissando = Some(addr.clone());
359 }
360 if note.glissando_end
361 && let Some(start) = open_glissando.take()
362 {
363 spans.push(SpanMark::Glissando {
364 start,
365 end: addr.clone(),
366 });
367 }
368
369 if let Some(chord) = ¬e.chord_symbol
370 && let Some(end) = &chord.range_end
371 {
372 spans.push(SpanMark::Harmony {
373 label: chord.display_text(),
374 start: addr.clone(),
375 end: end.clone(),
376 });
377 }
378 }
379 }
380 }
381 }
382 }
383
384 spans.extend(
385 typed_spanners
386 .iter()
387 .filter(|span| span.kind == NotationSpannerKind::Slur)
388 .map(|span| SpanMark::Slur {
389 start: span.start.clone(),
390 end: span.end.clone(),
391 }),
392 );
393 spans
394}
395
396fn collect_beam_groups(score: &Score, infer_unmarked: bool) -> Vec<BeamGroup> {
399 let mut groups = Vec::new();
400 for (pi, part) in score.parts.iter().enumerate() {
401 for (si, staff) in part.staves.iter().enumerate() {
402 let mut meter = score.settings.time_signature.clone();
403 for (mi, measure) in staff.measures.iter().enumerate() {
404 if let Some(change) = &measure.time_sig {
405 meter = change.clone();
406 }
407 for vi in 0..4usize {
408 let voice = &measure.voices[vi];
409 let mut current: Option<Vec<usize>> = None;
410
411 for (ni, note) in voice.iter().enumerate() {
412 match note.beam {
413 BeamState::Begin => {
414 current = Some(vec![ni]);
415 }
416 BeamState::BeginEnd => {
417 groups.push(BeamGroup {
418 part: pi,
419 staff: si,
420 measure: mi,
421 voice: vi,
422 note_indices: vec![ni],
423 });
424 }
425 BeamState::Continue
426 | BeamState::ForwardHook
427 | BeamState::BackwardHook => {
428 if let Some(ref mut g) = current {
429 g.push(ni);
430 }
431 }
432 BeamState::End => {
433 if let Some(mut g) = current.take() {
434 g.push(ni);
435 groups.push(BeamGroup {
436 part: pi,
437 staff: si,
438 measure: mi,
439 voice: vi,
440 note_indices: g,
441 });
442 }
443 }
444 BeamState::None => {
445 current = None;
446 }
447 }
448 }
449 if infer_unmarked {
450 let recommended = compute_beams(voice, &meter);
451 let mut candidate = Vec::new();
452 let mut flush = |candidate: &mut Vec<usize>| {
453 if candidate.len() >= 2 {
454 groups.push(BeamGroup {
455 part: pi,
456 staff: si,
457 measure: mi,
458 voice: vi,
459 note_indices: std::mem::take(candidate),
460 });
461 } else {
462 candidate.clear();
463 }
464 };
465 for (ni, (note, suggested)) in voice.iter().zip(&recommended).enumerate() {
466 if matches!(suggested, BeamState::Begin | BeamState::None)
467 || matches!(
468 note.tuplet_boundary,
469 Some(TupletBoundary::Start | TupletBoundary::StartStop)
470 )
471 {
472 flush(&mut candidate);
473 }
474 if *suggested != BeamState::None
475 && note.beam == BeamState::None
476 && !note.explicit_unbeamed
477 && !note.is_grace
478 && !note.is_cue
479 && !note.hidden
480 {
481 candidate.push(ni);
482 } else {
483 flush(&mut candidate);
484 }
485 if *suggested == BeamState::End
486 || matches!(
487 note.tuplet_boundary,
488 Some(TupletBoundary::Stop | TupletBoundary::StartStop)
489 )
490 {
491 flush(&mut candidate);
492 }
493 }
494 flush(&mut candidate);
495 }
496 }
497 }
498 }
499 }
500 groups
501}
502
503fn collect_tuplet_groups(score: &Score) -> Vec<TupletGroup> {
506 let mut groups = Vec::new();
507 for (pi, part) in score.parts.iter().enumerate() {
508 for (si, staff) in part.staves.iter().enumerate() {
509 for (mi, measure) in staff.measures.iter().enumerate() {
510 for vi in 0..4usize {
511 let voice = &measure.voices[vi];
512 for (first, last) in acorde_core::tuplet_group_ranges(voice) {
513 let note_indices: Vec<_> = (first..=last)
514 .filter(|&index| {
515 !voice[index].is_grace && voice[index].tuplet.is_some()
516 })
517 .collect();
518 if note_indices.len() >= 2
519 && let Some(ti) = voice[first].tuplet.as_ref()
520 {
521 groups.push(TupletGroup {
522 part: pi,
523 staff: si,
524 measure: mi,
525 voice: vi,
526 note_indices,
527 actual_notes: ti.actual_notes,
528 normal_notes: ti.normal_notes,
529 });
530 }
531 }
532 }
533 }
534 }
535 }
536 groups
537}
538
539fn step_idx(step: &Step) -> u8 {
540 match step {
541 Step::C => 0,
542 Step::D => 1,
543 Step::E => 2,
544 Step::F => 3,
545 Step::G => 4,
546 Step::A => 5,
547 Step::B => 6,
548 }
549}
550
551fn key_alter(fifths: i8, step: &Step) -> i8 {
553 const SHARPS: [Step; 7] = [
554 Step::F,
555 Step::C,
556 Step::G,
557 Step::D,
558 Step::A,
559 Step::E,
560 Step::B,
561 ];
562 const FLATS: [Step; 7] = [
563 Step::B,
564 Step::E,
565 Step::A,
566 Step::D,
567 Step::G,
568 Step::C,
569 Step::F,
570 ];
571 if fifths > 0 && SHARPS[..fifths as usize].contains(step) {
572 1
573 } else if fifths < 0 && FLATS[..(-fifths) as usize].contains(step) {
574 -1
575 } else {
576 0
577 }
578}
579
580fn collect_courtesy_accidentals(score: &Score) -> Vec<CourtesyAccidental> {
581 let mut result = Vec::new();
582 let base_fifths = score.settings.key_signature.fifths;
583
584 for (pi, part) in score.parts.iter().enumerate() {
585 for (si, staff) in part.staves.iter().enumerate() {
586 let mut prev_alters: HashMap<(u8, i8), i8> = HashMap::new();
588 let mut current_fifths = base_fifths;
589
590 for (mi, measure) in staff.measures.iter().enumerate() {
591 if let Some(key_sig) = measure.key_sig.as_ref() {
592 current_fifths = key_sig.fifths;
593 prev_alters.clear();
594 }
595
596 for vi in 0..4usize {
598 for (ni, note) in measure.voices[vi].iter().enumerate() {
599 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
600 let key = (step_idx(&pitch.step), pitch.octave);
601 let requested = note.accidental_display(pitch_idx)
603 == AccidentalDisplay::Parenthesized;
604 if requested || (!note.tie_end && prev_alters.contains_key(&key)) {
605 result.push(CourtesyAccidental {
606 part: pi,
607 staff: si,
608 measure: mi,
609 voice: vi,
610 note_index: ni,
611 pitch_index: pitch_idx,
612 alter: pitch.alter,
613 });
614 }
615 }
616 }
617 }
618
619 prev_alters.clear();
621 for vi in 0..4usize {
622 for note in measure.voices[vi].iter() {
623 for pitch in note.pitches.iter() {
624 if pitch.alter != key_alter(current_fifths, &pitch.step) {
625 prev_alters
626 .insert((step_idx(&pitch.step), pitch.octave), pitch.alter);
627 }
628 }
629 }
630 }
631 }
632 }
633 }
634 result
635}
636
637fn collect_accidental_marks(score: &Score) -> Vec<AccidentalMark> {
646 let mut result = Vec::new();
647 let base_fifths = score.settings.key_signature.fifths;
648
649 for (pi, part) in score.parts.iter().enumerate() {
650 for (si, staff) in part.staves.iter().enumerate() {
651 let mut current_fifths = base_fifths;
652
653 for (mi, measure) in staff.measures.iter().enumerate() {
654 if let Some(key_sig) = measure.key_sig.as_ref() {
655 current_fifths = key_sig.fifths;
656 }
657
658 let mut active: HashMap<(u8, i8), i8> = HashMap::new();
660
661 for vi in 0..4usize {
662 for (ni, note) in measure.voices[vi].iter().enumerate() {
663 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
664 let key = (step_idx(&pitch.step), pitch.octave);
665 let baseline = active
666 .get(&key)
667 .copied()
668 .unwrap_or_else(|| key_alter(current_fifths, &pitch.step));
669 let display = note.accidental_display(pitch_idx);
670 let parenthesized = display == AccidentalDisplay::Parenthesized;
671 if pitch.alter != baseline {
672 if !note.tie_end && !parenthesized {
674 result.push(AccidentalMark {
675 part: pi,
676 staff: si,
677 measure: mi,
678 voice: vi,
679 note_index: ni,
680 pitch_index: pitch_idx,
681 alter: pitch.alter,
682 });
683 }
684 active.insert(key, pitch.alter);
685 } else if matches!(
686 display,
687 AccidentalDisplay::Cautionary | AccidentalDisplay::Editorial
688 ) {
689 result.push(AccidentalMark {
692 part: pi,
693 staff: si,
694 measure: mi,
695 voice: vi,
696 note_index: ni,
697 pitch_index: pitch_idx,
698 alter: pitch.alter,
699 });
700 }
701 }
702 }
703 }
704 }
705 }
706 }
707 result
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use acorde_core::{Clef, Measure, Part, Score, Staff};
714
715 fn score_with_measures(n: usize) -> Score {
716 let mut score = Score::default();
717 score.parts.clear();
718 let mut staff = Staff::new(Clef::Treble);
719 for i in 0..n {
720 let mut m = Measure::empty(4, 4);
721 m.number = i as u32 + 1;
722 staff.measures.push(m);
723 }
724 let mut part = Part::new("P1", "P1");
725 part.staves.push(staff);
726 score.parts.push(part);
727 score
728 }
729
730 #[test]
731 fn default_score_has_one_row_of_four() {
732 let score = Score::default();
734 let result = compute_layout(&score, &LayoutConfig::default());
735 assert_eq!(result.rows.len(), 1);
736 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
737 assert_eq!(result.vis_slots.len(), 4);
738 assert!(result.spans.is_empty());
739 }
740
741 #[test]
742 fn vis_slots_expand_multi_rest() {
743 let mut score = score_with_measures(1);
744 score.parts[0].staves[0].measures[0].multi_rest_count = Some(4);
745
746 let result = compute_layout(
747 &score,
748 &LayoutConfig {
749 measures_per_row: 8,
750 concert_pitch: false,
751 first_row_measures: None,
752 },
753 );
754 assert_eq!(result.vis_slots.len(), 4);
755 assert!(result.vis_slots.iter().all(|&v| v == 0));
756 }
757
758 #[test]
759 fn measures_split_into_rows() {
760 let score = score_with_measures(6);
761 let result = compute_layout(
762 &score,
763 &LayoutConfig {
764 measures_per_row: 4,
765 concert_pitch: false,
766 first_row_measures: None,
767 },
768 );
769 assert_eq!(result.rows.len(), 2);
770 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
771 assert_eq!(result.rows[1].measure_indices, vec![4, 5]);
772 }
773
774 #[test]
775 fn single_measure_single_row() {
776 let score = score_with_measures(1);
777 let result = compute_layout(
778 &score,
779 &LayoutConfig {
780 measures_per_row: 4,
781 concert_pitch: false,
782 first_row_measures: None,
783 },
784 );
785 assert_eq!(result.rows.len(), 1);
786 assert_eq!(result.rows[0].measure_indices, vec![0]);
787 }
788
789 #[test]
790 fn exactly_full_rows() {
791 let score = score_with_measures(8);
792 let result = compute_layout(
793 &score,
794 &LayoutConfig {
795 measures_per_row: 4,
796 concert_pitch: false,
797 first_row_measures: None,
798 },
799 );
800 assert_eq!(result.rows.len(), 2);
801 assert_eq!(result.rows[0].measure_indices.len(), 4);
802 assert_eq!(result.rows[1].measure_indices.len(), 4);
803 }
804
805 #[test]
806 fn concert_pitch_bb_clarinet_transposes_key() {
807 let mut score = score_with_measures(1);
810 score.parts[0].staves[0].transpose_semitones = -2;
811 let result = compute_layout(
812 &score,
813 &LayoutConfig {
814 measures_per_row: 4,
815 concert_pitch: true,
816 first_row_measures: None,
817 },
818 );
819 assert_eq!(result.concert_key_overrides.len(), 1);
820 assert_eq!(result.concert_key_overrides[0].part_index, 0);
821 assert_eq!(result.concert_key_overrides[0].staff_index, 0);
822 assert_eq!(result.concert_key_overrides[0].fifths, -2);
823 }
824
825 #[test]
826 fn concert_pitch_false_no_override() {
827 let mut score = score_with_measures(1);
828 score.parts[0].staves[0].transpose_semitones = -2;
829 let result = compute_layout(
830 &score,
831 &LayoutConfig {
832 measures_per_row: 4,
833 concert_pitch: false,
834 first_row_measures: None,
835 },
836 );
837 assert!(result.concert_key_overrides.is_empty());
838 }
839
840 #[test]
841 fn concert_pitch_zero_transpose_no_override() {
842 let score = score_with_measures(1);
844 let result = compute_layout(
845 &score,
846 &LayoutConfig {
847 measures_per_row: 4,
848 concert_pitch: true,
849 first_row_measures: None,
850 },
851 );
852 assert!(result.concert_key_overrides.is_empty());
853 }
854
855 #[test]
856 fn transpose_key_formula_eb_alto_sax() {
857 assert_eq!(transpose_key_fifths(0, -9), -3);
860 }
861
862 #[test]
863 fn transpose_key_formula_perfect_fifth_up() {
864 assert_eq!(transpose_key_fifths(0, 7), 1);
866 }
867
868 #[test]
869 fn system_break_splits_row() {
870 let mut score = score_with_measures(3);
872 score.parts[0].staves[0].measures[1].system_break = true;
873 let result = compute_layout(
874 &score,
875 &LayoutConfig {
876 measures_per_row: 4,
877 concert_pitch: false,
878 first_row_measures: None,
879 },
880 );
881 assert_eq!(result.rows.len(), 2);
882 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
883 assert_eq!(result.rows[1].measure_indices, vec![2]);
884 }
885
886 #[test]
887 fn page_break_splits_row() {
888 let mut score = score_with_measures(3);
889 score.parts[0].staves[0].measures[0].page_break = true;
890 let result = compute_layout(
891 &score,
892 &LayoutConfig {
893 measures_per_row: 4,
894 concert_pitch: false,
895 first_row_measures: None,
896 },
897 );
898 assert_eq!(result.rows.len(), 2);
899 assert_eq!(result.rows[0].measure_indices, vec![0]);
900 assert_eq!(result.rows[1].measure_indices, vec![1, 2]);
901 }
902
903 #[test]
904 fn system_break_overrides_per_row() {
905 let mut score = score_with_measures(5);
907 score.parts[0].staves[0].measures[1].system_break = true;
908 let result = compute_layout(
909 &score,
910 &LayoutConfig {
911 measures_per_row: 4,
912 concert_pitch: false,
913 first_row_measures: None,
914 },
915 );
916 assert_eq!(result.rows.len(), 2);
917 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
918 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
919 }
920
921 #[test]
922 fn no_break_unchanged() {
923 let score = score_with_measures(3);
924 let result = compute_layout(
925 &score,
926 &LayoutConfig {
927 measures_per_row: 4,
928 concert_pitch: false,
929 first_row_measures: None,
930 },
931 );
932 assert_eq!(result.rows.len(), 1);
933 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2]);
934 }
935
936 #[test]
939 fn first_row_measures_limits_first_row() {
940 let score = score_with_measures(5);
942 let result = compute_layout(
943 &score,
944 &LayoutConfig {
945 measures_per_row: 4,
946 concert_pitch: false,
947 first_row_measures: Some(2),
948 },
949 );
950 assert_eq!(result.rows.len(), 2);
951 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
952 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
953 }
954
955 #[test]
956 fn first_row_measures_none_unchanged() {
957 let score = score_with_measures(5);
959 let without = compute_layout(
960 &score,
961 &LayoutConfig {
962 measures_per_row: 4,
963 concert_pitch: false,
964 first_row_measures: None,
965 },
966 );
967 let with_none = compute_layout(&score, &LayoutConfig::default());
968 assert_eq!(without.rows.len(), with_none.rows.len());
969 for (a, b) in without.rows.iter().zip(with_none.rows.iter()) {
970 assert_eq!(a.measure_indices, b.measure_indices);
971 }
972 }
973
974 #[test]
975 fn beam_groups_empty_for_default_score() {
976 let score = Score::default();
977 let result = compute_layout(&score, &LayoutConfig::default());
978 assert!(result.beam_groups.is_empty());
979 }
980
981 #[test]
982 fn display_beams_follow_simple_and_compound_meter_without_changing_score() {
983 use acorde_core::{Duration, Note, Pitch, Step, TimeSignature};
984 let note = || Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
985 let mut score = score_with_measures(2);
986 score.parts[0].staves[0].measures[0].voices[0] = vec![note(); 4];
987 score.parts[0].staves[0].measures[1].time_sig = Some(TimeSignature {
988 numerator: 6,
989 denominator: 8,
990 });
991 score.parts[0].staves[0].measures[1].voices[0] = vec![note(); 6];
992 let original = score.clone();
993
994 assert!(
995 compute_layout(&score, &LayoutConfig::default())
996 .beam_groups
997 .is_empty()
998 );
999 let layout = compute_layout_with_display_beams(&score, &LayoutConfig::default());
1000 let groups: Vec<_> = layout
1001 .beam_groups
1002 .iter()
1003 .map(|g| (g.measure, g.note_indices.clone()))
1004 .collect();
1005 assert_eq!(
1006 groups,
1007 vec![
1008 (0, vec![0, 1]),
1009 (0, vec![2, 3]),
1010 (1, vec![0, 1, 2]),
1011 (1, vec![3, 4, 5])
1012 ]
1013 );
1014 assert_eq!(
1015 serde_json::to_value(&score).unwrap(),
1016 serde_json::to_value(&original).unwrap()
1017 );
1018 }
1019
1020 #[test]
1021 fn display_beams_respect_authored_unbeamed_rest_grace_and_barlines() {
1022 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
1023 let note = || Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
1024 let mut score = score_with_measures(2);
1025 let mut authored = note();
1026 authored.beam = BeamState::Begin;
1027 let mut authored_end = note();
1028 authored_end.beam = BeamState::End;
1029 let mut no = note();
1030 no.explicit_unbeamed = true;
1031 let mut grace = note();
1032 grace.is_grace = true;
1033 score.parts[0].staves[0].measures[0].voices[0] = vec![
1034 authored,
1035 authored_end,
1036 no,
1037 note(),
1038 Note::rest(Duration::Eighth),
1039 note(),
1040 grace,
1041 note(),
1042 ];
1043 score.parts[0].staves[0].measures[1].voices[0] = vec![note(), note()];
1044
1045 let layout = compute_layout_with_display_beams(&score, &LayoutConfig::default());
1046 let groups: Vec<_> = layout
1047 .beam_groups
1048 .iter()
1049 .map(|g| (g.measure, g.note_indices.clone()))
1050 .collect();
1051 assert_eq!(groups, vec![(0, vec![0, 1]), (1, vec![0, 1])]);
1052 }
1053
1054 #[test]
1055 fn display_beams_keep_pickup_and_tuplet_boundaries_separate() {
1056 use acorde_core::{Duration, Note, Pitch, Step, TupletBoundary, TupletInfo};
1057 let mut score = score_with_measures(2);
1058 let mut notes: Vec<_> = (0..6)
1059 .map(|_| Note::new(Pitch::new(Step::C, 4), Duration::Eighth))
1060 .collect();
1061 for note in &mut notes[..3] {
1062 note.tuplet = Some(TupletInfo {
1063 actual_notes: 3,
1064 normal_notes: 2,
1065 });
1066 }
1067 notes[0].tuplet_boundary = Some(TupletBoundary::Start);
1068 notes[2].tuplet_boundary = Some(TupletBoundary::Stop);
1069 score.parts[0].staves[0].measures[0].voices[0] = notes;
1071 score.parts[0].staves[0].measures[1].voices[0] = (0..2)
1072 .map(|_| Note::new(Pitch::new(Step::D, 4), Duration::Eighth))
1073 .collect();
1074
1075 let layout = compute_layout_with_display_beams(&score, &LayoutConfig::default());
1076 assert_eq!(layout.beam_groups[0].note_indices, vec![0, 1, 2]);
1077 assert!(
1078 layout
1079 .beam_groups
1080 .iter()
1081 .any(|group| group.measure == 1 && group.note_indices == vec![0, 1])
1082 );
1083 assert!(
1084 layout
1085 .beam_groups
1086 .iter()
1087 .all(|g| !g.note_indices.contains(&2) || !g.note_indices.contains(&3))
1088 );
1089 }
1090
1091 #[test]
1092 fn layout_exposes_source_voice_number_for_canonical_slot() {
1093 let mut score = score_with_measures(1);
1094 score.parts[0].staves[0].measures[0].source_voice_numbers[1] = Some(5);
1095
1096 let result = compute_layout(&score, &LayoutConfig::default());
1097 assert_eq!(
1098 result.source_voice_addresses,
1099 vec![SourceVoiceAddress {
1100 part: 0,
1101 staff: 0,
1102 measure: 0,
1103 voice: 1,
1104 source_voice_number: 5,
1105 }]
1106 );
1107 }
1108
1109 #[test]
1110 fn beam_groups_begin_end_collected() {
1111 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
1112 let mut score = score_with_measures(1);
1113 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
1114 n1.beam = BeamState::Begin;
1115 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
1116 n2.beam = BeamState::Continue;
1117 let mut n3 = Note::new(Pitch::new(Step::E, 4), Duration::Eighth);
1118 n3.beam = BeamState::End;
1119 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2, n3];
1120
1121 let result = compute_layout(&score, &LayoutConfig::default());
1122 assert_eq!(result.beam_groups.len(), 1);
1123 assert_eq!(result.beam_groups[0].note_indices, vec![0, 1, 2]);
1124 assert_eq!(result.beam_groups[0].part, 0);
1125 assert_eq!(result.beam_groups[0].measure, 0);
1126 assert_eq!(result.beam_groups[0].voice, 0);
1127 }
1128
1129 #[test]
1130 fn beam_groups_beginend_is_standalone_group() {
1131 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
1132 let mut score = score_with_measures(1);
1133 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
1134 n.beam = BeamState::BeginEnd;
1135 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
1136
1137 let result = compute_layout(&score, &LayoutConfig::default());
1138 assert_eq!(result.beam_groups.len(), 1);
1139 assert_eq!(result.beam_groups[0].note_indices, vec![0]);
1140 }
1141
1142 #[test]
1143 fn tuplet_groups_empty_for_default_score() {
1144 let score = Score::default();
1145 let result = compute_layout(&score, &LayoutConfig::default());
1146 assert!(result.tuplet_groups.is_empty());
1147 }
1148
1149 #[test]
1150 fn tuplet_groups_triplet_collected() {
1151 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
1152 let ti = TupletInfo {
1153 actual_notes: 3,
1154 normal_notes: 2,
1155 };
1156 let mut score = score_with_measures(1);
1157 let notes: Vec<Note> = (0..3)
1158 .map(|i| {
1159 let step = [Step::C, Step::D, Step::E][i].clone();
1160 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
1161 n.tuplet = Some(ti.clone());
1162 n
1163 })
1164 .collect();
1165 score.parts[0].staves[0].measures[0].voices[0] = notes;
1166
1167 let result = compute_layout(&score, &LayoutConfig::default());
1168 assert_eq!(result.tuplet_groups.len(), 1);
1169 assert_eq!(result.tuplet_groups[0].note_indices, vec![0, 1, 2]);
1170 assert_eq!(result.tuplet_groups[0].actual_notes, 3);
1171 assert_eq!(result.tuplet_groups[0].normal_notes, 2);
1172 assert_eq!(result.tuplet_groups[0].part, 0);
1173 assert_eq!(result.tuplet_groups[0].measure, 0);
1174 }
1175
1176 #[test]
1177 fn tuplet_groups_no_group_for_single_tuplet_note() {
1178 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
1179 let mut score = score_with_measures(1);
1180 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1181 n.tuplet = Some(TupletInfo {
1182 actual_notes: 3,
1183 normal_notes: 2,
1184 });
1185 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
1186
1187 let result = compute_layout(&score, &LayoutConfig::default());
1188 assert!(result.tuplet_groups.is_empty());
1190 }
1191
1192 #[test]
1193 fn hairpin_span_resolved() {
1194 use acorde_core::{Duration, HairpinKind, Note, Pitch, Step};
1195 let mut score = score_with_measures(1);
1196 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1197 n1.hairpin_start = Some(HairpinKind::Crescendo);
1198 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
1199 n2.hairpin_end = true;
1200 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
1201
1202 let result = compute_layout(&score, &LayoutConfig::default());
1203 assert_eq!(result.spans.len(), 1);
1204 if let SpanMark::Hairpin { kind, start, end } = &result.spans[0] {
1205 assert_eq!(*kind, HairpinKind::Crescendo);
1206 assert_eq!(start.note, 0);
1207 assert_eq!(end.note, 1);
1208 } else {
1209 panic!("expected Hairpin span");
1210 }
1211 }
1212
1213 #[test]
1214 fn slur_span_resolved() {
1215 use acorde_core::{Duration, Note, Pitch, Step};
1216 let mut score = score_with_measures(1);
1217 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1218 n1.slur_start = true;
1219 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
1220 n2.slur_end = true;
1221 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
1222
1223 let result = compute_layout(&score, &LayoutConfig::default());
1224 let slur = result
1225 .spans
1226 .iter()
1227 .find(|s| matches!(s, SpanMark::Slur { .. }));
1228 assert!(slur.is_some(), "expected a Slur span");
1229 if let Some(SpanMark::Slur { start, end }) = slur {
1230 assert_eq!(start.note, 0);
1231 assert_eq!(end.note, 1);
1232 }
1233 }
1234
1235 #[test]
1236 fn adjacent_legacy_slurs_share_an_endpoint_without_zero_length() {
1237 use acorde_core::{Duration, Note, Pitch, Step};
1238 let mut score = score_with_measures(1);
1239 let mut notes = [Step::C, Step::D, Step::E, Step::F]
1240 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter));
1241 notes[0].slur_start = true;
1242 notes[1].slur_end = true;
1243 notes[1].slur_start = true;
1244 notes[2].slur_end = true;
1245 score.parts[0].staves[0].measures[0].voices[0] = notes.to_vec();
1246 let slurs = compute_layout(&score, &LayoutConfig::default())
1247 .spans
1248 .into_iter()
1249 .filter_map(|span| match span {
1250 SpanMark::Slur { start, end } => Some((start.note, end.note)),
1251 _ => None,
1252 })
1253 .collect::<Vec<_>>();
1254 assert_eq!(slurs, vec![(0, 1), (1, 2)]);
1255 }
1256
1257 #[test]
1258 fn nested_legacy_slurs_close_in_last_in_first_out_order() {
1259 use acorde_core::{Duration, Note, Pitch, Step};
1260 let mut score = score_with_measures(1);
1261 let mut notes = [Step::C, Step::D, Step::E, Step::F]
1262 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter));
1263 notes[0].slur_start = true;
1264 notes[1].slur_start = true;
1265 notes[2].slur_end = true;
1266 notes[3].slur_end = true;
1267 score.parts[0].staves[0].measures[0].voices[0] = notes.to_vec();
1268 let slurs = compute_layout(&score, &LayoutConfig::default())
1269 .spans
1270 .into_iter()
1271 .filter_map(|span| match span {
1272 SpanMark::Slur { start, end } => Some((start.note, end.note)),
1273 _ => None,
1274 })
1275 .collect::<Vec<_>>();
1276 assert_eq!(slurs, vec![(1, 2), (0, 3)]);
1277 }
1278
1279 #[test]
1280 fn typed_slurs_keep_overlapping_pairs_without_legacy_duplicates() {
1281 use acorde_core::{Duration, Note, Pitch, Step};
1282 let mut score = score_with_measures(1);
1283 let mut notes = [Step::C, Step::D, Step::E, Step::F]
1284 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter));
1285 notes[0].slur_start = true;
1286 notes[1].slur_start = true;
1287 notes[2].slur_end = true;
1288 notes[3].slur_end = true;
1289 score.parts[0].staves[0].measures[0].voices[0] = notes.to_vec();
1290 for (id, start, end) in [("outer", 0, 2), ("overlap", 1, 3)] {
1291 score.spanners.push(NotationSpanner {
1292 id: id.into(),
1293 kind: NotationSpannerKind::Slur,
1294 start: NoteAddr {
1295 part: 0,
1296 staff: 0,
1297 measure: 0,
1298 voice: 0,
1299 note: start,
1300 },
1301 end: NoteAddr {
1302 part: 0,
1303 staff: 0,
1304 measure: 0,
1305 voice: 0,
1306 note: end,
1307 },
1308 number: None,
1309 line_type: None,
1310 text: None,
1311 placement: None,
1312 ottava_size: None,
1313 ottava_type: None,
1314 });
1315 }
1316 let slurs = compute_layout(&score, &LayoutConfig::default())
1317 .spans
1318 .into_iter()
1319 .filter_map(|span| match span {
1320 SpanMark::Slur { start, end } => Some((start.note, end.note)),
1321 _ => None,
1322 })
1323 .collect::<Vec<_>>();
1324 assert_eq!(slurs, vec![(0, 2), (1, 3)]);
1325 }
1326
1327 #[test]
1328 fn typed_spanners_keep_stable_identity_in_layout_result() {
1329 use acorde_core::{Duration, NotationSpanner, NotationSpannerKind, Note, Pitch, Step};
1330 let mut score = score_with_measures(1);
1331 score.parts[0].staves[0].measures[0].voices[0] = vec![
1332 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
1333 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
1334 ];
1335 score.spanners.push(NotationSpanner {
1336 id: "typed-slur-7".to_string(),
1337 kind: NotationSpannerKind::Slur,
1338 start: NoteAddr {
1339 part: 0,
1340 staff: 0,
1341 measure: 0,
1342 voice: 0,
1343 note: 0,
1344 },
1345 end: NoteAddr {
1346 part: 0,
1347 staff: 0,
1348 measure: 0,
1349 voice: 0,
1350 note: 1,
1351 },
1352 number: Some(7),
1353 line_type: Some("dashed".to_string()),
1354 text: None,
1355 placement: Some("above".to_string()),
1356 ottava_size: None,
1357 ottava_type: None,
1358 });
1359
1360 let result = compute_layout(&score, &LayoutConfig::default());
1361 assert_eq!(result.typed_spanners.len(), 1);
1362 assert_eq!(result.typed_spanners[0].id, "typed-slur-7");
1363 assert_eq!(result.typed_spanners[0].number, Some(7));
1364 }
1365
1366 #[test]
1367 fn harmony_range_span_resolved_from_typed_end_address() {
1368 use acorde_core::{ChordSymbol, Duration, Note, NoteAddr, Pitch, Step};
1369 let mut score = score_with_measures(2);
1370 score.parts[0].staves[0].measures[0].voices[0] =
1371 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
1372 score.parts[0].staves[0].measures[1].voices[0] =
1373 vec![Note::new(Pitch::new(Step::G, 4), Duration::Whole)];
1374 score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
1375 root: "C".to_owned(),
1376 kind: "major".to_owned(),
1377 bass: None,
1378 placement: None,
1379 extender: true,
1380 harmonic_degree: None,
1381 harmony_function: None,
1382 harmony_type: None,
1383 chord_ref: None,
1384 range_end: Some(NoteAddr {
1385 part: 0,
1386 staff: 0,
1387 measure: 1,
1388 voice: 0,
1389 note: 0,
1390 }),
1391 degrees: Vec::new(),
1392 });
1393
1394 let result = compute_layout(&score, &LayoutConfig::default());
1395 assert!(matches!(
1396 result.spans.as_slice(),
1397 [SpanMark::Harmony { label, start, end }]
1398 if label == "C" && start.measure == 0 && end.measure == 1
1399 ));
1400 }
1401
1402 #[test]
1405 fn courtesy_none_for_clean_score() {
1406 let score = Score::default();
1408 let result = compute_layout(&score, &LayoutConfig::default());
1409 assert!(result.courtesy_accidentals.is_empty());
1410 }
1411
1412 #[test]
1413 fn courtesy_after_chromatic_alteration() {
1414 use acorde_core::{Duration, Note, Pitch, Step};
1417 let mut score = score_with_measures(2);
1418
1419 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1420 fsharp.pitches[0].alter = 1;
1421 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1422
1423 let fnat = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1424 score.parts[0].staves[0].measures[1].voices[0] = vec![fnat];
1426
1427 let result = compute_layout(&score, &LayoutConfig::default());
1428 assert_eq!(result.courtesy_accidentals.len(), 1);
1429 let ca = &result.courtesy_accidentals[0];
1430 assert_eq!(ca.measure, 1);
1431 assert_eq!(ca.alter, 0); }
1433
1434 #[test]
1435 fn courtesy_same_alteration_no_courtesy() {
1436 use acorde_core::{Duration, Note, Pitch, Step};
1442 let mut score = score_with_measures(2);
1443
1444 let mut fsharp1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1445 fsharp1.pitches[0].alter = 1;
1446 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp1];
1447
1448 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1449 fsharp2.pitches[0].alter = 1;
1450 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
1451
1452 let result = compute_layout(&score, &LayoutConfig::default());
1453 assert_eq!(result.courtesy_accidentals.len(), 1);
1455 assert_eq!(result.courtesy_accidentals[0].alter, 1);
1456 }
1457
1458 #[test]
1459 fn courtesy_tied_note_excluded() {
1460 use acorde_core::{Duration, Note, Pitch, Step};
1462 let mut score = score_with_measures(2);
1463
1464 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1465 fsharp.pitches[0].alter = 1;
1466 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1467
1468 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1469 tied.pitches[0].alter = 1;
1470 tied.tie_end = true;
1471 score.parts[0].staves[0].measures[1].voices[0] = vec![tied];
1472
1473 let result = compute_layout(&score, &LayoutConfig::default());
1474 assert!(result.courtesy_accidentals.is_empty());
1475 }
1476
1477 #[test]
1478 fn courtesy_key_change_resets() {
1479 use acorde_core::{Duration, KeySignature, Note, Pitch, Step};
1482 let mut score = score_with_measures(2);
1483
1484 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1485 fsharp.pitches[0].alter = 1;
1486 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1487
1488 score.parts[0].staves[0].measures[1].key_sig = Some(KeySignature {
1490 fifths: 1,
1491 mode: "major".to_string(),
1492 });
1493 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1495 fsharp2.pitches[0].alter = 1;
1496 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
1497
1498 let result = compute_layout(&score, &LayoutConfig::default());
1499 assert!(result.courtesy_accidentals.is_empty());
1501 }
1502
1503 #[test]
1506 fn requested_accidentals_are_drawn_where_the_rules_imply_them() {
1507 use acorde_core::{Duration, Note, Pitch, Step};
1508 let mut score = score_with_measures(1);
1509 let mut cautionary = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1510 cautionary.set_accidental_display(0, AccidentalDisplay::Cautionary);
1511 let mut sharp = Note::new(Pitch::with_alter(Step::F, 4, 1), Duration::Quarter);
1512 sharp.set_accidental_display(0, AccidentalDisplay::Parenthesized);
1513 let mut bracketed = Note::new(Pitch::new(Step::G, 4), Duration::Quarter);
1514 bracketed.set_accidental_display(0, AccidentalDisplay::Parenthesized);
1515 let plain = Note::new(Pitch::new(Step::A, 4), Duration::Quarter);
1516 score.parts[0].staves[0].measures[0].voices[0] = vec![cautionary, sharp, bracketed, plain];
1517
1518 let result = compute_layout(&score, &LayoutConfig::default());
1519 let mandatory: Vec<usize> = result.accidentals.iter().map(|a| a.note_index).collect();
1520 let courtesy: Vec<usize> = result
1521 .courtesy_accidentals
1522 .iter()
1523 .map(|a| a.note_index)
1524 .collect();
1525 assert_eq!(mandatory, vec![0]);
1528 assert_eq!(courtesy, vec![1, 2]);
1529 }
1530
1531 #[test]
1532 fn accidental_none_for_clean_score() {
1533 let score = Score::default();
1534 let result = compute_layout(&score, &LayoutConfig::default());
1535 assert!(result.accidentals.is_empty());
1536 }
1537
1538 #[test]
1539 fn accidental_first_chromatic_note_in_measure() {
1540 use acorde_core::{Duration, Note, Pitch, Step};
1542 let mut score = score_with_measures(1);
1543 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1544 fsharp.pitches[0].alter = 1;
1545 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1546
1547 let result = compute_layout(&score, &LayoutConfig::default());
1548 assert_eq!(result.accidentals.len(), 1);
1549 assert_eq!(result.accidentals[0].alter, 1);
1550 assert_eq!(result.accidentals[0].measure, 0);
1551 }
1552
1553 #[test]
1554 fn accidental_repeat_same_alter_in_measure_not_marked_again() {
1555 use acorde_core::{Duration, Note, Pitch, Step};
1557 let mut score = score_with_measures(1);
1558 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1559 f1.pitches[0].alter = 1;
1560 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1561 f2.pitches[0].alter = 1;
1562 score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
1563
1564 let result = compute_layout(&score, &LayoutConfig::default());
1565 assert_eq!(result.accidentals.len(), 1);
1566 }
1567
1568 #[test]
1569 fn accidental_reverts_to_natural_within_same_measure() {
1570 use acorde_core::{Duration, Note, Pitch, Step};
1572 let mut score = score_with_measures(1);
1573 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1574 f1.pitches[0].alter = 1;
1575 let f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter); score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
1577
1578 let result = compute_layout(&score, &LayoutConfig::default());
1579 assert_eq!(result.accidentals.len(), 2);
1580 assert_eq!(result.accidentals[0].alter, 1);
1581 assert_eq!(result.accidentals[1].alter, 0);
1582 }
1583
1584 #[test]
1585 fn accidental_does_not_carry_across_barline() {
1586 use acorde_core::{Duration, Note, Pitch, Step};
1590 let mut score = score_with_measures(2);
1591 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1592 f1.pitches[0].alter = 1;
1593 score.parts[0].staves[0].measures[0].voices[0] = vec![f1];
1594 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1595 f2.pitches[0].alter = 1;
1596 score.parts[0].staves[0].measures[1].voices[0] = vec![f2];
1597
1598 let result = compute_layout(&score, &LayoutConfig::default());
1599 assert_eq!(result.accidentals.len(), 2);
1600 assert!(result.accidentals.iter().any(|a| a.measure == 1));
1601 assert_eq!(result.courtesy_accidentals.len(), 1);
1603 assert_eq!(result.courtesy_accidentals[0].measure, 1);
1604 }
1605
1606 #[test]
1607 fn accidental_tied_note_excluded() {
1608 use acorde_core::{Duration, Note, Pitch, Step};
1609 let mut score = score_with_measures(1);
1610 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1611 tied.pitches[0].alter = 1;
1612 tied.tie_end = true;
1613 score.parts[0].staves[0].measures[0].voices[0] = vec![tied];
1614
1615 let result = compute_layout(&score, &LayoutConfig::default());
1616 assert!(result.accidentals.is_empty());
1617 }
1618
1619 #[test]
1620 fn accidental_shared_across_voices_on_same_staff() {
1621 use acorde_core::{Duration, Note, Pitch, Step};
1623 let mut score = score_with_measures(1);
1624 let mut f_v0 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1625 f_v0.pitches[0].alter = 1;
1626 score.parts[0].staves[0].measures[0].voices[0] = vec![f_v0];
1627 let mut f_v1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1628 f_v1.pitches[0].alter = 1;
1629 score.parts[0].staves[0].measures[0].voices[1] = vec![f_v1];
1630
1631 let result = compute_layout(&score, &LayoutConfig::default());
1632 assert_eq!(result.accidentals.len(), 1);
1633 }
1634
1635 #[test]
1636 fn accidental_double_sharp_marked() {
1637 use acorde_core::{Duration, Note, Pitch, Step};
1638 let mut score = score_with_measures(1);
1639 let mut n = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1640 n.pitches[0].alter = 2;
1641 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
1642
1643 let result = compute_layout(&score, &LayoutConfig::default());
1644 assert_eq!(result.accidentals.len(), 1);
1645 assert_eq!(result.accidentals[0].alter, 2);
1646 }
1647}