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 let onset: f64 = measure.voices[voice]
317 .iter()
318 .take(ni)
319 .map(|note| note.beats())
320 .sum();
321 let timed_here = |down| {
322 measure.timed_pedals.iter().any(|pedal| {
323 pedal.down == down && (pedal.position_beats - onset).abs() < 1e-9
324 })
325 };
326 if note.pedal_start && !timed_here(true) {
327 open_pedal = Some(addr.clone());
328 }
329 if note.pedal_end
330 && !timed_here(false)
331 && let Some(start) = open_pedal.take()
332 {
333 spans.push(SpanMark::Pedal {
334 start,
335 end: addr.clone(),
336 });
337 }
338
339 if !typed_slur_endpoints.contains(&(pi, si, mi, voice, ni)) {
343 if note.slur_end
344 && let Some(start) = open_slurs.pop()
345 {
346 spans.push(SpanMark::Slur {
347 start,
348 end: addr.clone(),
349 });
350 }
351 if note.slur_start {
352 open_slurs.push(addr.clone());
353 }
354 }
355
356 if note.trill_line_start {
357 open_trill_line = Some(addr.clone());
358 }
359 if note.trill_line_end
360 && let Some(start) = open_trill_line.take()
361 {
362 spans.push(SpanMark::TrillLine {
363 start,
364 end: addr.clone(),
365 });
366 }
367
368 if note.glissando_start {
369 open_glissando = Some(addr.clone());
370 }
371 if note.glissando_end
372 && let Some(start) = open_glissando.take()
373 {
374 spans.push(SpanMark::Glissando {
375 start,
376 end: addr.clone(),
377 });
378 }
379
380 if let Some(chord) = ¬e.chord_symbol
381 && let Some(end) = &chord.range_end
382 {
383 spans.push(SpanMark::Harmony {
384 label: chord.display_text(),
385 start: addr.clone(),
386 end: end.clone(),
387 });
388 }
389 }
390 }
391 }
392 }
393 }
394
395 spans.extend(
396 typed_spanners
397 .iter()
398 .filter(|span| span.kind == NotationSpannerKind::Slur)
399 .map(|span| SpanMark::Slur {
400 start: span.start.clone(),
401 end: span.end.clone(),
402 }),
403 );
404 spans
405}
406
407fn collect_beam_groups(score: &Score, infer_unmarked: bool) -> Vec<BeamGroup> {
410 let mut groups = Vec::new();
411 for (pi, part) in score.parts.iter().enumerate() {
412 for (si, staff) in part.staves.iter().enumerate() {
413 let mut meter = score.settings.time_signature.clone();
414 for (mi, measure) in staff.measures.iter().enumerate() {
415 if let Some(change) = &measure.time_sig {
416 meter = change.clone();
417 }
418 for vi in 0..4usize {
419 let voice = &measure.voices[vi];
420 let mut current: Option<Vec<usize>> = None;
421
422 for (ni, note) in voice.iter().enumerate() {
423 match note.beam {
424 BeamState::Begin => {
425 current = Some(vec![ni]);
426 }
427 BeamState::BeginEnd => {
428 groups.push(BeamGroup {
429 part: pi,
430 staff: si,
431 measure: mi,
432 voice: vi,
433 note_indices: vec![ni],
434 });
435 }
436 BeamState::Continue
437 | BeamState::ForwardHook
438 | BeamState::BackwardHook => {
439 if let Some(ref mut g) = current {
440 g.push(ni);
441 }
442 }
443 BeamState::End => {
444 if let Some(mut g) = current.take() {
445 g.push(ni);
446 groups.push(BeamGroup {
447 part: pi,
448 staff: si,
449 measure: mi,
450 voice: vi,
451 note_indices: g,
452 });
453 }
454 }
455 BeamState::None => {
456 current = None;
457 }
458 }
459 }
460 if infer_unmarked {
461 let recommended = compute_beams(voice, &meter);
462 let mut candidate = Vec::new();
463 let mut flush = |candidate: &mut Vec<usize>| {
464 if candidate.len() >= 2 {
465 groups.push(BeamGroup {
466 part: pi,
467 staff: si,
468 measure: mi,
469 voice: vi,
470 note_indices: std::mem::take(candidate),
471 });
472 } else {
473 candidate.clear();
474 }
475 };
476 for (ni, (note, suggested)) in voice.iter().zip(&recommended).enumerate() {
477 if matches!(suggested, BeamState::Begin | BeamState::None)
478 || matches!(
479 note.tuplet_boundary,
480 Some(TupletBoundary::Start | TupletBoundary::StartStop)
481 )
482 {
483 flush(&mut candidate);
484 }
485 if *suggested != BeamState::None
486 && note.beam == BeamState::None
487 && !note.explicit_unbeamed
488 && !note.is_grace
489 && !note.is_cue
490 && !note.hidden
491 {
492 candidate.push(ni);
493 } else {
494 flush(&mut candidate);
495 }
496 if *suggested == BeamState::End
497 || matches!(
498 note.tuplet_boundary,
499 Some(TupletBoundary::Stop | TupletBoundary::StartStop)
500 )
501 {
502 flush(&mut candidate);
503 }
504 }
505 flush(&mut candidate);
506 }
507 }
508 }
509 }
510 }
511 groups
512}
513
514fn collect_tuplet_groups(score: &Score) -> Vec<TupletGroup> {
517 let mut groups = Vec::new();
518 for (pi, part) in score.parts.iter().enumerate() {
519 for (si, staff) in part.staves.iter().enumerate() {
520 for (mi, measure) in staff.measures.iter().enumerate() {
521 for vi in 0..4usize {
522 let voice = &measure.voices[vi];
523 for (first, last) in acorde_core::tuplet_group_ranges(voice) {
524 let note_indices: Vec<_> = (first..=last)
525 .filter(|&index| {
526 !voice[index].is_grace && voice[index].tuplet.is_some()
527 })
528 .collect();
529 if note_indices.len() >= 2
530 && let Some(ti) = voice[first].tuplet.as_ref()
531 {
532 groups.push(TupletGroup {
533 part: pi,
534 staff: si,
535 measure: mi,
536 voice: vi,
537 note_indices,
538 actual_notes: ti.actual_notes,
539 normal_notes: ti.normal_notes,
540 });
541 }
542 }
543 }
544 }
545 }
546 }
547 groups
548}
549
550fn step_idx(step: &Step) -> u8 {
551 match step {
552 Step::C => 0,
553 Step::D => 1,
554 Step::E => 2,
555 Step::F => 3,
556 Step::G => 4,
557 Step::A => 5,
558 Step::B => 6,
559 }
560}
561
562fn key_alter(fifths: i8, step: &Step) -> i8 {
564 const SHARPS: [Step; 7] = [
565 Step::F,
566 Step::C,
567 Step::G,
568 Step::D,
569 Step::A,
570 Step::E,
571 Step::B,
572 ];
573 const FLATS: [Step; 7] = [
574 Step::B,
575 Step::E,
576 Step::A,
577 Step::D,
578 Step::G,
579 Step::C,
580 Step::F,
581 ];
582 if fifths > 0 && SHARPS[..fifths as usize].contains(step) {
583 1
584 } else if fifths < 0 && FLATS[..(-fifths) as usize].contains(step) {
585 -1
586 } else {
587 0
588 }
589}
590
591fn collect_courtesy_accidentals(score: &Score) -> Vec<CourtesyAccidental> {
592 let mut result = Vec::new();
593 let base_fifths = score.settings.key_signature.fifths;
594
595 for (pi, part) in score.parts.iter().enumerate() {
596 for (si, staff) in part.staves.iter().enumerate() {
597 let mut prev_alters: HashMap<(u8, i8), i8> = HashMap::new();
599 let mut current_fifths = base_fifths;
600
601 for (mi, measure) in staff.measures.iter().enumerate() {
602 if let Some(key_sig) = measure.key_sig.as_ref() {
603 current_fifths = key_sig.fifths;
604 prev_alters.clear();
605 }
606
607 for vi in 0..4usize {
609 for (ni, note) in measure.voices[vi].iter().enumerate() {
610 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
611 let key = (step_idx(&pitch.step), pitch.octave);
612 let requested = note.accidental_display(pitch_idx)
614 == AccidentalDisplay::Parenthesized;
615 let previous = prev_alters.remove(&key);
619 let natural_reminder = !note.tie_end
620 && note.accidental_display(pitch_idx) == AccidentalDisplay::Auto
621 && pitch.alter == 0
622 && key_alter(current_fifths, &pitch.step) == 0
623 && previous.is_some_and(|alter| alter != 0);
624 if requested || natural_reminder {
625 result.push(CourtesyAccidental {
626 part: pi,
627 staff: si,
628 measure: mi,
629 voice: vi,
630 note_index: ni,
631 pitch_index: pitch_idx,
632 alter: pitch.alter,
633 });
634 }
635 }
636 }
637 }
638
639 prev_alters.clear();
641 for vi in 0..4usize {
642 for note in measure.voices[vi].iter() {
643 for pitch in note.pitches.iter() {
644 if pitch.alter != key_alter(current_fifths, &pitch.step) {
645 prev_alters
646 .insert((step_idx(&pitch.step), pitch.octave), pitch.alter);
647 }
648 }
649 }
650 }
651 }
652 }
653 }
654 result
655}
656
657fn collect_accidental_marks(score: &Score) -> Vec<AccidentalMark> {
666 let mut result = Vec::new();
667 let base_fifths = score.settings.key_signature.fifths;
668
669 for (pi, part) in score.parts.iter().enumerate() {
670 for (si, staff) in part.staves.iter().enumerate() {
671 let mut current_fifths = base_fifths;
672
673 for (mi, measure) in staff.measures.iter().enumerate() {
674 if let Some(key_sig) = measure.key_sig.as_ref() {
675 current_fifths = key_sig.fifths;
676 }
677
678 let mut active: HashMap<(u8, i8), i8> = HashMap::new();
680
681 for vi in 0..4usize {
682 for (ni, note) in measure.voices[vi].iter().enumerate() {
683 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
684 let key = (step_idx(&pitch.step), pitch.octave);
685 let baseline = active
686 .get(&key)
687 .copied()
688 .unwrap_or_else(|| key_alter(current_fifths, &pitch.step));
689 let display = note.accidental_display(pitch_idx);
690 let parenthesized = display == AccidentalDisplay::Parenthesized;
691 if pitch.alter != baseline {
692 if (!note.tie_end || display == AccidentalDisplay::Explicit)
694 && !parenthesized
695 {
696 result.push(AccidentalMark {
697 part: pi,
698 staff: si,
699 measure: mi,
700 voice: vi,
701 note_index: ni,
702 pitch_index: pitch_idx,
703 alter: pitch.alter,
704 });
705 }
706 active.insert(key, pitch.alter);
707 } else if matches!(
708 display,
709 AccidentalDisplay::Explicit
710 | AccidentalDisplay::Cautionary
711 | AccidentalDisplay::Editorial
712 ) {
713 result.push(AccidentalMark {
716 part: pi,
717 staff: si,
718 measure: mi,
719 voice: vi,
720 note_index: ni,
721 pitch_index: pitch_idx,
722 alter: pitch.alter,
723 });
724 }
725 }
726 }
727 }
728 }
729 }
730 }
731 result
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use acorde_core::{Clef, Measure, Part, Score, Staff};
738
739 fn score_with_measures(n: usize) -> Score {
740 let mut score = Score::default();
741 score.parts.clear();
742 let mut staff = Staff::new(Clef::Treble);
743 for i in 0..n {
744 let mut m = Measure::empty(4, 4);
745 m.number = i as u32 + 1;
746 staff.measures.push(m);
747 }
748 let mut part = Part::new("P1", "P1");
749 part.staves.push(staff);
750 score.parts.push(part);
751 score
752 }
753
754 #[test]
755 fn default_score_has_one_row_of_four() {
756 let score = Score::default();
758 let result = compute_layout(&score, &LayoutConfig::default());
759 assert_eq!(result.rows.len(), 1);
760 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
761 assert_eq!(result.vis_slots.len(), 4);
762 assert!(result.spans.is_empty());
763 }
764
765 #[test]
766 fn vis_slots_expand_multi_rest() {
767 let mut score = score_with_measures(1);
768 score.parts[0].staves[0].measures[0].multi_rest_count = Some(4);
769
770 let result = compute_layout(
771 &score,
772 &LayoutConfig {
773 measures_per_row: 8,
774 concert_pitch: false,
775 first_row_measures: None,
776 },
777 );
778 assert_eq!(result.vis_slots.len(), 4);
779 assert!(result.vis_slots.iter().all(|&v| v == 0));
780 }
781
782 #[test]
783 fn measures_split_into_rows() {
784 let score = score_with_measures(6);
785 let result = compute_layout(
786 &score,
787 &LayoutConfig {
788 measures_per_row: 4,
789 concert_pitch: false,
790 first_row_measures: None,
791 },
792 );
793 assert_eq!(result.rows.len(), 2);
794 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
795 assert_eq!(result.rows[1].measure_indices, vec![4, 5]);
796 }
797
798 #[test]
799 fn single_measure_single_row() {
800 let score = score_with_measures(1);
801 let result = compute_layout(
802 &score,
803 &LayoutConfig {
804 measures_per_row: 4,
805 concert_pitch: false,
806 first_row_measures: None,
807 },
808 );
809 assert_eq!(result.rows.len(), 1);
810 assert_eq!(result.rows[0].measure_indices, vec![0]);
811 }
812
813 #[test]
814 fn exactly_full_rows() {
815 let score = score_with_measures(8);
816 let result = compute_layout(
817 &score,
818 &LayoutConfig {
819 measures_per_row: 4,
820 concert_pitch: false,
821 first_row_measures: None,
822 },
823 );
824 assert_eq!(result.rows.len(), 2);
825 assert_eq!(result.rows[0].measure_indices.len(), 4);
826 assert_eq!(result.rows[1].measure_indices.len(), 4);
827 }
828
829 #[test]
830 fn concert_pitch_bb_clarinet_transposes_key() {
831 let mut score = score_with_measures(1);
834 score.parts[0].staves[0].transpose_semitones = -2;
835 let result = compute_layout(
836 &score,
837 &LayoutConfig {
838 measures_per_row: 4,
839 concert_pitch: true,
840 first_row_measures: None,
841 },
842 );
843 assert_eq!(result.concert_key_overrides.len(), 1);
844 assert_eq!(result.concert_key_overrides[0].part_index, 0);
845 assert_eq!(result.concert_key_overrides[0].staff_index, 0);
846 assert_eq!(result.concert_key_overrides[0].fifths, -2);
847 }
848
849 #[test]
850 fn concert_pitch_false_no_override() {
851 let mut score = score_with_measures(1);
852 score.parts[0].staves[0].transpose_semitones = -2;
853 let result = compute_layout(
854 &score,
855 &LayoutConfig {
856 measures_per_row: 4,
857 concert_pitch: false,
858 first_row_measures: None,
859 },
860 );
861 assert!(result.concert_key_overrides.is_empty());
862 }
863
864 #[test]
865 fn concert_pitch_zero_transpose_no_override() {
866 let score = score_with_measures(1);
868 let result = compute_layout(
869 &score,
870 &LayoutConfig {
871 measures_per_row: 4,
872 concert_pitch: true,
873 first_row_measures: None,
874 },
875 );
876 assert!(result.concert_key_overrides.is_empty());
877 }
878
879 #[test]
880 fn transpose_key_formula_eb_alto_sax() {
881 assert_eq!(transpose_key_fifths(0, -9), -3);
884 }
885
886 #[test]
887 fn transpose_key_formula_perfect_fifth_up() {
888 assert_eq!(transpose_key_fifths(0, 7), 1);
890 }
891
892 #[test]
893 fn system_break_splits_row() {
894 let mut score = score_with_measures(3);
896 score.parts[0].staves[0].measures[1].system_break = true;
897 let result = compute_layout(
898 &score,
899 &LayoutConfig {
900 measures_per_row: 4,
901 concert_pitch: false,
902 first_row_measures: None,
903 },
904 );
905 assert_eq!(result.rows.len(), 2);
906 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
907 assert_eq!(result.rows[1].measure_indices, vec![2]);
908 }
909
910 #[test]
911 fn page_break_splits_row() {
912 let mut score = score_with_measures(3);
913 score.parts[0].staves[0].measures[0].page_break = true;
914 let result = compute_layout(
915 &score,
916 &LayoutConfig {
917 measures_per_row: 4,
918 concert_pitch: false,
919 first_row_measures: None,
920 },
921 );
922 assert_eq!(result.rows.len(), 2);
923 assert_eq!(result.rows[0].measure_indices, vec![0]);
924 assert_eq!(result.rows[1].measure_indices, vec![1, 2]);
925 }
926
927 #[test]
928 fn system_break_overrides_per_row() {
929 let mut score = score_with_measures(5);
931 score.parts[0].staves[0].measures[1].system_break = true;
932 let result = compute_layout(
933 &score,
934 &LayoutConfig {
935 measures_per_row: 4,
936 concert_pitch: false,
937 first_row_measures: None,
938 },
939 );
940 assert_eq!(result.rows.len(), 2);
941 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
942 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
943 }
944
945 #[test]
946 fn no_break_unchanged() {
947 let score = score_with_measures(3);
948 let result = compute_layout(
949 &score,
950 &LayoutConfig {
951 measures_per_row: 4,
952 concert_pitch: false,
953 first_row_measures: None,
954 },
955 );
956 assert_eq!(result.rows.len(), 1);
957 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2]);
958 }
959
960 #[test]
963 fn first_row_measures_limits_first_row() {
964 let score = score_with_measures(5);
966 let result = compute_layout(
967 &score,
968 &LayoutConfig {
969 measures_per_row: 4,
970 concert_pitch: false,
971 first_row_measures: Some(2),
972 },
973 );
974 assert_eq!(result.rows.len(), 2);
975 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
976 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
977 }
978
979 #[test]
980 fn first_row_measures_none_unchanged() {
981 let score = score_with_measures(5);
983 let without = compute_layout(
984 &score,
985 &LayoutConfig {
986 measures_per_row: 4,
987 concert_pitch: false,
988 first_row_measures: None,
989 },
990 );
991 let with_none = compute_layout(&score, &LayoutConfig::default());
992 assert_eq!(without.rows.len(), with_none.rows.len());
993 for (a, b) in without.rows.iter().zip(with_none.rows.iter()) {
994 assert_eq!(a.measure_indices, b.measure_indices);
995 }
996 }
997
998 #[test]
999 fn beam_groups_empty_for_default_score() {
1000 let score = Score::default();
1001 let result = compute_layout(&score, &LayoutConfig::default());
1002 assert!(result.beam_groups.is_empty());
1003 }
1004
1005 #[test]
1006 fn display_beams_follow_simple_and_compound_meter_without_changing_score() {
1007 use acorde_core::{Duration, Note, Pitch, Step, TimeSignature};
1008 let note = || Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
1009 let mut score = score_with_measures(2);
1010 score.parts[0].staves[0].measures[0].voices[0] = vec![note(); 4];
1011 score.parts[0].staves[0].measures[1].time_sig = Some(TimeSignature {
1012 numerator: 6,
1013 denominator: 8,
1014 });
1015 score.parts[0].staves[0].measures[1].voices[0] = vec![note(); 6];
1016 let original = score.clone();
1017
1018 assert!(
1019 compute_layout(&score, &LayoutConfig::default())
1020 .beam_groups
1021 .is_empty()
1022 );
1023 let layout = compute_layout_with_display_beams(&score, &LayoutConfig::default());
1024 let groups: Vec<_> = layout
1025 .beam_groups
1026 .iter()
1027 .map(|g| (g.measure, g.note_indices.clone()))
1028 .collect();
1029 assert_eq!(
1030 groups,
1031 vec![
1032 (0, vec![0, 1]),
1033 (0, vec![2, 3]),
1034 (1, vec![0, 1, 2]),
1035 (1, vec![3, 4, 5])
1036 ]
1037 );
1038 assert_eq!(
1039 serde_json::to_value(&score).unwrap(),
1040 serde_json::to_value(&original).unwrap()
1041 );
1042 }
1043
1044 #[test]
1045 fn display_beams_respect_authored_unbeamed_rest_grace_and_barlines() {
1046 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
1047 let note = || Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
1048 let mut score = score_with_measures(2);
1049 let mut authored = note();
1050 authored.beam = BeamState::Begin;
1051 let mut authored_end = note();
1052 authored_end.beam = BeamState::End;
1053 let mut no = note();
1054 no.explicit_unbeamed = true;
1055 let mut grace = note();
1056 grace.is_grace = true;
1057 score.parts[0].staves[0].measures[0].voices[0] = vec![
1058 authored,
1059 authored_end,
1060 no,
1061 note(),
1062 Note::rest(Duration::Eighth),
1063 note(),
1064 grace,
1065 note(),
1066 ];
1067 score.parts[0].staves[0].measures[1].voices[0] = vec![note(), note()];
1068
1069 let layout = compute_layout_with_display_beams(&score, &LayoutConfig::default());
1070 let groups: Vec<_> = layout
1071 .beam_groups
1072 .iter()
1073 .map(|g| (g.measure, g.note_indices.clone()))
1074 .collect();
1075 assert_eq!(groups, vec![(0, vec![0, 1]), (1, vec![0, 1])]);
1076 }
1077
1078 #[test]
1079 fn display_beams_keep_pickup_and_tuplet_boundaries_separate() {
1080 use acorde_core::{Duration, Note, Pitch, Step, TupletBoundary, TupletInfo};
1081 let mut score = score_with_measures(2);
1082 let mut notes: Vec<_> = (0..6)
1083 .map(|_| Note::new(Pitch::new(Step::C, 4), Duration::Eighth))
1084 .collect();
1085 for note in &mut notes[..3] {
1086 note.tuplet = Some(TupletInfo {
1087 actual_notes: 3,
1088 normal_notes: 2,
1089 });
1090 }
1091 notes[0].tuplet_boundary = Some(TupletBoundary::Start);
1092 notes[2].tuplet_boundary = Some(TupletBoundary::Stop);
1093 score.parts[0].staves[0].measures[0].voices[0] = notes;
1095 score.parts[0].staves[0].measures[1].voices[0] = (0..2)
1096 .map(|_| Note::new(Pitch::new(Step::D, 4), Duration::Eighth))
1097 .collect();
1098
1099 let layout = compute_layout_with_display_beams(&score, &LayoutConfig::default());
1100 assert_eq!(layout.beam_groups[0].note_indices, vec![0, 1, 2]);
1101 assert!(
1102 layout
1103 .beam_groups
1104 .iter()
1105 .any(|group| group.measure == 1 && group.note_indices == vec![0, 1])
1106 );
1107 assert!(
1108 layout
1109 .beam_groups
1110 .iter()
1111 .all(|g| !g.note_indices.contains(&2) || !g.note_indices.contains(&3))
1112 );
1113 }
1114
1115 #[test]
1116 fn layout_exposes_source_voice_number_for_canonical_slot() {
1117 let mut score = score_with_measures(1);
1118 score.parts[0].staves[0].measures[0].source_voice_numbers[1] = Some(5);
1119
1120 let result = compute_layout(&score, &LayoutConfig::default());
1121 assert_eq!(
1122 result.source_voice_addresses,
1123 vec![SourceVoiceAddress {
1124 part: 0,
1125 staff: 0,
1126 measure: 0,
1127 voice: 1,
1128 source_voice_number: 5,
1129 }]
1130 );
1131 }
1132
1133 #[test]
1134 fn beam_groups_begin_end_collected() {
1135 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
1136 let mut score = score_with_measures(1);
1137 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
1138 n1.beam = BeamState::Begin;
1139 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
1140 n2.beam = BeamState::Continue;
1141 let mut n3 = Note::new(Pitch::new(Step::E, 4), Duration::Eighth);
1142 n3.beam = BeamState::End;
1143 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2, n3];
1144
1145 let result = compute_layout(&score, &LayoutConfig::default());
1146 assert_eq!(result.beam_groups.len(), 1);
1147 assert_eq!(result.beam_groups[0].note_indices, vec![0, 1, 2]);
1148 assert_eq!(result.beam_groups[0].part, 0);
1149 assert_eq!(result.beam_groups[0].measure, 0);
1150 assert_eq!(result.beam_groups[0].voice, 0);
1151 }
1152
1153 #[test]
1154 fn beam_groups_beginend_is_standalone_group() {
1155 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
1156 let mut score = score_with_measures(1);
1157 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
1158 n.beam = BeamState::BeginEnd;
1159 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
1160
1161 let result = compute_layout(&score, &LayoutConfig::default());
1162 assert_eq!(result.beam_groups.len(), 1);
1163 assert_eq!(result.beam_groups[0].note_indices, vec![0]);
1164 }
1165
1166 #[test]
1167 fn tuplet_groups_empty_for_default_score() {
1168 let score = Score::default();
1169 let result = compute_layout(&score, &LayoutConfig::default());
1170 assert!(result.tuplet_groups.is_empty());
1171 }
1172
1173 #[test]
1174 fn tuplet_groups_triplet_collected() {
1175 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
1176 let ti = TupletInfo {
1177 actual_notes: 3,
1178 normal_notes: 2,
1179 };
1180 let mut score = score_with_measures(1);
1181 let notes: Vec<Note> = (0..3)
1182 .map(|i| {
1183 let step = [Step::C, Step::D, Step::E][i].clone();
1184 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
1185 n.tuplet = Some(ti.clone());
1186 n
1187 })
1188 .collect();
1189 score.parts[0].staves[0].measures[0].voices[0] = notes;
1190
1191 let result = compute_layout(&score, &LayoutConfig::default());
1192 assert_eq!(result.tuplet_groups.len(), 1);
1193 assert_eq!(result.tuplet_groups[0].note_indices, vec![0, 1, 2]);
1194 assert_eq!(result.tuplet_groups[0].actual_notes, 3);
1195 assert_eq!(result.tuplet_groups[0].normal_notes, 2);
1196 assert_eq!(result.tuplet_groups[0].part, 0);
1197 assert_eq!(result.tuplet_groups[0].measure, 0);
1198 }
1199
1200 #[test]
1201 fn tuplet_groups_no_group_for_single_tuplet_note() {
1202 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
1203 let mut score = score_with_measures(1);
1204 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1205 n.tuplet = Some(TupletInfo {
1206 actual_notes: 3,
1207 normal_notes: 2,
1208 });
1209 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
1210
1211 let result = compute_layout(&score, &LayoutConfig::default());
1212 assert!(result.tuplet_groups.is_empty());
1214 }
1215
1216 #[test]
1217 fn hairpin_span_resolved() {
1218 use acorde_core::{Duration, HairpinKind, Note, Pitch, Step};
1219 let mut score = score_with_measures(1);
1220 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1221 n1.hairpin_start = Some(HairpinKind::Crescendo);
1222 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
1223 n2.hairpin_end = true;
1224 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
1225
1226 let result = compute_layout(&score, &LayoutConfig::default());
1227 assert_eq!(result.spans.len(), 1);
1228 if let SpanMark::Hairpin { kind, start, end } = &result.spans[0] {
1229 assert_eq!(*kind, HairpinKind::Crescendo);
1230 assert_eq!(start.note, 0);
1231 assert_eq!(end.note, 1);
1232 } else {
1233 panic!("expected Hairpin span");
1234 }
1235 }
1236
1237 #[test]
1238 fn slur_span_resolved() {
1239 use acorde_core::{Duration, Note, Pitch, Step};
1240 let mut score = score_with_measures(1);
1241 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1242 n1.slur_start = true;
1243 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
1244 n2.slur_end = true;
1245 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
1246
1247 let result = compute_layout(&score, &LayoutConfig::default());
1248 let slur = result
1249 .spans
1250 .iter()
1251 .find(|s| matches!(s, SpanMark::Slur { .. }));
1252 assert!(slur.is_some(), "expected a Slur span");
1253 if let Some(SpanMark::Slur { start, end }) = slur {
1254 assert_eq!(start.note, 0);
1255 assert_eq!(end.note, 1);
1256 }
1257 }
1258
1259 #[test]
1260 fn adjacent_legacy_slurs_share_an_endpoint_without_zero_length() {
1261 use acorde_core::{Duration, Note, Pitch, Step};
1262 let mut score = score_with_measures(1);
1263 let mut notes = [Step::C, Step::D, Step::E, Step::F]
1264 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter));
1265 notes[0].slur_start = true;
1266 notes[1].slur_end = true;
1267 notes[1].slur_start = true;
1268 notes[2].slur_end = true;
1269 score.parts[0].staves[0].measures[0].voices[0] = notes.to_vec();
1270 let slurs = compute_layout(&score, &LayoutConfig::default())
1271 .spans
1272 .into_iter()
1273 .filter_map(|span| match span {
1274 SpanMark::Slur { start, end } => Some((start.note, end.note)),
1275 _ => None,
1276 })
1277 .collect::<Vec<_>>();
1278 assert_eq!(slurs, vec![(0, 1), (1, 2)]);
1279 }
1280
1281 #[test]
1282 fn nested_legacy_slurs_close_in_last_in_first_out_order() {
1283 use acorde_core::{Duration, Note, Pitch, Step};
1284 let mut score = score_with_measures(1);
1285 let mut notes = [Step::C, Step::D, Step::E, Step::F]
1286 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter));
1287 notes[0].slur_start = true;
1288 notes[1].slur_start = true;
1289 notes[2].slur_end = true;
1290 notes[3].slur_end = true;
1291 score.parts[0].staves[0].measures[0].voices[0] = notes.to_vec();
1292 let slurs = compute_layout(&score, &LayoutConfig::default())
1293 .spans
1294 .into_iter()
1295 .filter_map(|span| match span {
1296 SpanMark::Slur { start, end } => Some((start.note, end.note)),
1297 _ => None,
1298 })
1299 .collect::<Vec<_>>();
1300 assert_eq!(slurs, vec![(1, 2), (0, 3)]);
1301 }
1302
1303 #[test]
1304 fn typed_slurs_keep_overlapping_pairs_without_legacy_duplicates() {
1305 use acorde_core::{Duration, Note, Pitch, Step};
1306 let mut score = score_with_measures(1);
1307 let mut notes = [Step::C, Step::D, Step::E, Step::F]
1308 .map(|step| Note::new(Pitch::new(step, 4), Duration::Quarter));
1309 notes[0].slur_start = true;
1310 notes[1].slur_start = true;
1311 notes[2].slur_end = true;
1312 notes[3].slur_end = true;
1313 score.parts[0].staves[0].measures[0].voices[0] = notes.to_vec();
1314 for (id, start, end) in [("outer", 0, 2), ("overlap", 1, 3)] {
1315 score.spanners.push(NotationSpanner {
1316 id: id.into(),
1317 kind: NotationSpannerKind::Slur,
1318 start: NoteAddr {
1319 part: 0,
1320 staff: 0,
1321 measure: 0,
1322 voice: 0,
1323 note: start,
1324 },
1325 end: NoteAddr {
1326 part: 0,
1327 staff: 0,
1328 measure: 0,
1329 voice: 0,
1330 note: end,
1331 },
1332 number: None,
1333 line_type: None,
1334 text: None,
1335 placement: None,
1336 ottava_size: None,
1337 ottava_type: None,
1338 });
1339 }
1340 let slurs = compute_layout(&score, &LayoutConfig::default())
1341 .spans
1342 .into_iter()
1343 .filter_map(|span| match span {
1344 SpanMark::Slur { start, end } => Some((start.note, end.note)),
1345 _ => None,
1346 })
1347 .collect::<Vec<_>>();
1348 assert_eq!(slurs, vec![(0, 2), (1, 3)]);
1349 }
1350
1351 #[test]
1352 fn typed_spanners_keep_stable_identity_in_layout_result() {
1353 use acorde_core::{Duration, NotationSpanner, NotationSpannerKind, Note, Pitch, Step};
1354 let mut score = score_with_measures(1);
1355 score.parts[0].staves[0].measures[0].voices[0] = vec![
1356 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
1357 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
1358 ];
1359 score.spanners.push(NotationSpanner {
1360 id: "typed-slur-7".to_string(),
1361 kind: NotationSpannerKind::Slur,
1362 start: NoteAddr {
1363 part: 0,
1364 staff: 0,
1365 measure: 0,
1366 voice: 0,
1367 note: 0,
1368 },
1369 end: NoteAddr {
1370 part: 0,
1371 staff: 0,
1372 measure: 0,
1373 voice: 0,
1374 note: 1,
1375 },
1376 number: Some(7),
1377 line_type: Some("dashed".to_string()),
1378 text: None,
1379 placement: Some("above".to_string()),
1380 ottava_size: None,
1381 ottava_type: None,
1382 });
1383
1384 let result = compute_layout(&score, &LayoutConfig::default());
1385 assert_eq!(result.typed_spanners.len(), 1);
1386 assert_eq!(result.typed_spanners[0].id, "typed-slur-7");
1387 assert_eq!(result.typed_spanners[0].number, Some(7));
1388 }
1389
1390 #[test]
1391 fn harmony_range_span_resolved_from_typed_end_address() {
1392 use acorde_core::{ChordSymbol, Duration, Note, NoteAddr, Pitch, Step};
1393 let mut score = score_with_measures(2);
1394 score.parts[0].staves[0].measures[0].voices[0] =
1395 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
1396 score.parts[0].staves[0].measures[1].voices[0] =
1397 vec![Note::new(Pitch::new(Step::G, 4), Duration::Whole)];
1398 score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
1399 root: "C".to_owned(),
1400 kind: "major".to_owned(),
1401 bass: None,
1402 placement: None,
1403 extender: true,
1404 harmonic_degree: None,
1405 harmony_function: None,
1406 harmony_type: None,
1407 chord_ref: None,
1408 range_end: Some(NoteAddr {
1409 part: 0,
1410 staff: 0,
1411 measure: 1,
1412 voice: 0,
1413 note: 0,
1414 }),
1415 degrees: Vec::new(),
1416 });
1417
1418 let result = compute_layout(&score, &LayoutConfig::default());
1419 assert!(matches!(
1420 result.spans.as_slice(),
1421 [SpanMark::Harmony { label, start, end }]
1422 if label == "C" && start.measure == 0 && end.measure == 1
1423 ));
1424 }
1425
1426 #[test]
1429 fn courtesy_none_for_clean_score() {
1430 let score = Score::default();
1432 let result = compute_layout(&score, &LayoutConfig::default());
1433 assert!(result.courtesy_accidentals.is_empty());
1434 }
1435
1436 #[test]
1437 fn courtesy_after_chromatic_alteration() {
1438 use acorde_core::{Duration, Note, Pitch, Step};
1441 let mut score = score_with_measures(2);
1442
1443 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1444 fsharp.pitches[0].alter = 1;
1445 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1446
1447 let fnat = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1448 score.parts[0].staves[0].measures[1].voices[0] = vec![fnat];
1450
1451 let result = compute_layout(&score, &LayoutConfig::default());
1452 assert_eq!(result.courtesy_accidentals.len(), 1);
1453 let ca = &result.courtesy_accidentals[0];
1454 assert_eq!(ca.measure, 1);
1455 assert_eq!(ca.alter, 0); }
1457
1458 #[test]
1459 fn courtesy_same_alteration_no_courtesy() {
1460 use acorde_core::{Duration, Note, Pitch, Step};
1463 let mut score = score_with_measures(2);
1464
1465 let mut fsharp1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1466 fsharp1.pitches[0].alter = 1;
1467 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp1];
1468
1469 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1470 fsharp2.pitches[0].alter = 1;
1471 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
1472
1473 let result = compute_layout(&score, &LayoutConfig::default());
1474 assert!(result.courtesy_accidentals.is_empty());
1475 }
1476
1477 #[test]
1478 fn courtesy_tied_note_excluded() {
1479 use acorde_core::{Duration, Note, Pitch, Step};
1481 let mut score = score_with_measures(2);
1482
1483 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1484 fsharp.pitches[0].alter = 1;
1485 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1486
1487 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1488 tied.pitches[0].alter = 1;
1489 tied.tie_end = true;
1490 score.parts[0].staves[0].measures[1].voices[0] = vec![tied];
1491
1492 let result = compute_layout(&score, &LayoutConfig::default());
1493 assert!(result.courtesy_accidentals.is_empty());
1494 }
1495
1496 #[test]
1497 fn courtesy_key_change_resets() {
1498 use acorde_core::{Duration, KeySignature, Note, Pitch, Step};
1501 let mut score = score_with_measures(2);
1502
1503 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1504 fsharp.pitches[0].alter = 1;
1505 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1506
1507 score.parts[0].staves[0].measures[1].key_sig = Some(KeySignature {
1509 fifths: 1,
1510 mode: "major".to_string(),
1511 });
1512 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1514 fsharp2.pitches[0].alter = 1;
1515 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
1516
1517 let result = compute_layout(&score, &LayoutConfig::default());
1518 assert!(result.courtesy_accidentals.is_empty());
1520 }
1521
1522 #[test]
1525 fn requested_accidentals_are_drawn_where_the_rules_imply_them() {
1526 use acorde_core::{Duration, Note, Pitch, Step};
1527 let mut score = score_with_measures(1);
1528 let mut cautionary = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
1529 cautionary.set_accidental_display(0, AccidentalDisplay::Cautionary);
1530 let mut sharp = Note::new(Pitch::with_alter(Step::F, 4, 1), Duration::Quarter);
1531 sharp.set_accidental_display(0, AccidentalDisplay::Parenthesized);
1532 let mut bracketed = Note::new(Pitch::new(Step::G, 4), Duration::Quarter);
1533 bracketed.set_accidental_display(0, AccidentalDisplay::Parenthesized);
1534 let plain = Note::new(Pitch::new(Step::A, 4), Duration::Quarter);
1535 score.parts[0].staves[0].measures[0].voices[0] = vec![cautionary, sharp, bracketed, plain];
1536
1537 let result = compute_layout(&score, &LayoutConfig::default());
1538 let mandatory: Vec<usize> = result.accidentals.iter().map(|a| a.note_index).collect();
1539 let courtesy: Vec<usize> = result
1540 .courtesy_accidentals
1541 .iter()
1542 .map(|a| a.note_index)
1543 .collect();
1544 assert_eq!(mandatory, vec![0]);
1547 assert_eq!(courtesy, vec![1, 2]);
1548 }
1549
1550 #[test]
1551 fn accidental_none_for_clean_score() {
1552 let score = Score::default();
1553 let result = compute_layout(&score, &LayoutConfig::default());
1554 assert!(result.accidentals.is_empty());
1555 }
1556
1557 #[test]
1558 fn accidental_first_chromatic_note_in_measure() {
1559 use acorde_core::{Duration, Note, Pitch, Step};
1561 let mut score = score_with_measures(1);
1562 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1563 fsharp.pitches[0].alter = 1;
1564 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
1565
1566 let result = compute_layout(&score, &LayoutConfig::default());
1567 assert_eq!(result.accidentals.len(), 1);
1568 assert_eq!(result.accidentals[0].alter, 1);
1569 assert_eq!(result.accidentals[0].measure, 0);
1570 }
1571
1572 #[test]
1573 fn accidental_repeat_same_alter_in_measure_not_marked_again() {
1574 use acorde_core::{Duration, Note, Pitch, Step};
1576 let mut score = score_with_measures(1);
1577 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1578 f1.pitches[0].alter = 1;
1579 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1580 f2.pitches[0].alter = 1;
1581 score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
1582
1583 let result = compute_layout(&score, &LayoutConfig::default());
1584 assert_eq!(result.accidentals.len(), 1);
1585 }
1586
1587 #[test]
1588 fn accidental_reverts_to_natural_within_same_measure() {
1589 use acorde_core::{Duration, Note, Pitch, Step};
1591 let mut score = score_with_measures(1);
1592 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1593 f1.pitches[0].alter = 1;
1594 let f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter); score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
1596
1597 let result = compute_layout(&score, &LayoutConfig::default());
1598 assert_eq!(result.accidentals.len(), 2);
1599 assert_eq!(result.accidentals[0].alter, 1);
1600 assert_eq!(result.accidentals[1].alter, 0);
1601 }
1602
1603 #[test]
1604 fn accidental_does_not_carry_across_barline() {
1605 use acorde_core::{Duration, Note, Pitch, Step};
1609 let mut score = score_with_measures(2);
1610 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1611 f1.pitches[0].alter = 1;
1612 score.parts[0].staves[0].measures[0].voices[0] = vec![f1];
1613 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1614 f2.pitches[0].alter = 1;
1615 score.parts[0].staves[0].measures[1].voices[0] = vec![f2];
1616
1617 let result = compute_layout(&score, &LayoutConfig::default());
1618 assert_eq!(result.accidentals.len(), 2);
1619 assert!(result.accidentals.iter().any(|a| a.measure == 1));
1620 assert!(result.courtesy_accidentals.is_empty());
1621 }
1622
1623 #[test]
1624 fn accidental_tied_note_excluded() {
1625 use acorde_core::{Duration, Note, Pitch, Step};
1626 let mut score = score_with_measures(1);
1627 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1628 tied.pitches[0].alter = 1;
1629 tied.tie_end = true;
1630 score.parts[0].staves[0].measures[0].voices[0] = vec![tied];
1631
1632 let result = compute_layout(&score, &LayoutConfig::default());
1633 assert!(result.accidentals.is_empty());
1634 }
1635
1636 #[test]
1637 fn accidental_shared_across_voices_on_same_staff() {
1638 use acorde_core::{Duration, Note, Pitch, Step};
1640 let mut score = score_with_measures(1);
1641 let mut f_v0 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1642 f_v0.pitches[0].alter = 1;
1643 score.parts[0].staves[0].measures[0].voices[0] = vec![f_v0];
1644 let mut f_v1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1645 f_v1.pitches[0].alter = 1;
1646 score.parts[0].staves[0].measures[0].voices[1] = vec![f_v1];
1647
1648 let result = compute_layout(&score, &LayoutConfig::default());
1649 assert_eq!(result.accidentals.len(), 1);
1650 }
1651
1652 #[test]
1653 fn accidental_double_sharp_marked() {
1654 use acorde_core::{Duration, Note, Pitch, Step};
1655 let mut score = score_with_measures(1);
1656 let mut n = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
1657 n.pitches[0].alter = 2;
1658 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
1659
1660 let result = compute_layout(&score, &LayoutConfig::default());
1661 assert_eq!(result.accidentals.len(), 1);
1662 assert_eq!(result.accidentals[0].alter, 2);
1663 }
1664}