1use std::collections::HashMap;
2use acorde_core::{BeamState, HairpinKind, NoteAddr, OttavaKind, Score, Step, TupletInfo};
3use crate::{BeamGroup, ConcertKeyOverride, CourtesyAccidental, LayoutConfig, LayoutResult, RowLayout, SpanMark, TupletGroup};
4
5fn transpose_key_fifths(fifths: i8, semitones: i8) -> i8 {
10 let raw = (semitones as i16 * 7).rem_euclid(12) as i8;
11 let delta = if raw > 6 { raw - 12 } else { raw };
12 (fifths + delta).clamp(-7, 7)
13}
14
15pub fn compute_layout(score: &Score, config: &LayoutConfig) -> LayoutResult {
19 let per_row = config.measures_per_row.max(1);
20 let first_row = config.first_row_measures.map(|n| n.max(1));
21 let vis_slots = build_vis_slots(score);
22 let rows = build_rows(score, &vis_slots, per_row, first_row);
23 let spans = resolve_spans(score);
24
25 let mut concert_key_overrides = Vec::new();
26 if config.concert_pitch {
27 let base_fifths = score.settings.key_signature.fifths;
28 for (pi, part) in score.parts.iter().enumerate() {
29 for (si, staff) in part.staves.iter().enumerate() {
30 if staff.transpose_semitones != 0 {
31 concert_key_overrides.push(ConcertKeyOverride {
32 part_index: pi,
33 staff_index: si,
34 fifths: transpose_key_fifths(base_fifths, staff.transpose_semitones),
35 });
36 }
37 }
38 }
39 }
40
41 let beam_groups = collect_beam_groups(score);
42 let tuplet_groups = collect_tuplet_groups(score);
43 let courtesy_accidentals = collect_courtesy_accidentals(score);
44
45 LayoutResult { vis_slots, rows, spans, concert_key_overrides, beam_groups, tuplet_groups, courtesy_accidentals }
46}
47
48fn build_vis_slots(score: &Score) -> Vec<usize> {
51 let measure_count = score.parts.first()
52 .and_then(|p| p.staves.first())
53 .map(|s| s.measures.len())
54 .unwrap_or(0);
55
56 let mut slots = Vec::new();
57 for phys in 0..measure_count {
58 let count = multi_rest_count(score, phys).max(1);
59 for _ in 0..count {
60 slots.push(phys);
61 }
62 }
63 slots
64}
65
66fn multi_rest_count(score: &Score, measure_index: usize) -> usize {
67 score.parts.iter()
68 .flat_map(|p| p.staves.iter())
69 .filter_map(|s| s.measures.get(measure_index))
70 .filter_map(|m| m.multi_rest_count)
71 .map(|c| c as usize)
72 .max()
73 .unwrap_or(1)
74}
75
76fn force_break_after(score: &Score, phys: usize) -> bool {
79 score.parts.iter()
80 .flat_map(|p| p.staves.iter())
81 .filter_map(|s| s.measures.get(phys))
82 .any(|m| m.system_break || m.page_break)
83}
84
85fn build_rows(
86 score: &Score,
87 vis_slots: &[usize],
88 per_row: usize,
89 first_row_limit: Option<usize>,
90) -> Vec<RowLayout> {
91 if vis_slots.is_empty() {
92 return vec![RowLayout { measure_indices: Vec::new() }];
93 }
94
95 let mut rows: Vec<RowLayout> = Vec::new();
96 let mut current: Vec<usize> = Vec::new();
97 let mut last_phys = usize::MAX;
98
99 for &phys in vis_slots {
100 if phys != last_phys {
101 let limit = if rows.is_empty() {
102 first_row_limit.unwrap_or(per_row)
103 } else {
104 per_row
105 };
106 if current.len() == limit {
107 rows.push(RowLayout { measure_indices: std::mem::take(&mut current) });
108 }
109 current.push(phys);
110 if force_break_after(score, phys) {
111 rows.push(RowLayout { measure_indices: std::mem::take(&mut current) });
112 }
113 }
114 last_phys = phys;
115 }
116
117 if !current.is_empty() {
118 rows.push(RowLayout { measure_indices: current });
119 }
120 rows
121}
122
123fn resolve_spans(score: &Score) -> Vec<SpanMark> {
126 let mut spans = Vec::new();
127
128 for (pi, part) in score.parts.iter().enumerate() {
129 for (si, staff) in part.staves.iter().enumerate() {
130 for voice in 0..4usize {
131 let mut open_hairpin: Option<(NoteAddr, HairpinKind)> = None;
132 let mut open_ottava: Option<(NoteAddr, OttavaKind)> = None;
133 let mut open_pedal: Option<NoteAddr> = None;
134 let mut open_slur: Option<NoteAddr> = None;
135 let mut open_trill_line: Option<NoteAddr> = None;
136
137 for (mi, measure) in staff.measures.iter().enumerate() {
138 for (ni, note) in measure.voices[voice].iter().enumerate() {
139 let addr = NoteAddr { part: pi, staff: si, measure: mi, voice, note: ni };
140
141 if let Some(kind) = note.hairpin_start {
142 open_hairpin = Some((addr.clone(), kind));
143 }
144 if note.hairpin_end
145 && let Some((start, kind)) = open_hairpin.take() {
146 spans.push(SpanMark::Hairpin { kind, start, end: addr.clone() });
147 }
148
149 if let Some(kind) = note.ottava_start {
150 open_ottava = Some((addr.clone(), kind));
151 }
152 if note.ottava_end
153 && let Some((start, kind)) = open_ottava.take() {
154 spans.push(SpanMark::Ottava { kind, start, end: addr.clone() });
155 }
156
157 if note.pedal_start {
158 open_pedal = Some(addr.clone());
159 }
160 if note.pedal_end
161 && let Some(start) = open_pedal.take() {
162 spans.push(SpanMark::Pedal { start, end: addr.clone() });
163 }
164
165 if note.slur_start {
166 open_slur = Some(addr.clone());
167 }
168 if note.slur_end
169 && let Some(start) = open_slur.take() {
170 spans.push(SpanMark::Slur { start, end: addr.clone() });
171 }
172
173 if note.trill_line_start {
174 open_trill_line = Some(addr.clone());
175 }
176 if note.trill_line_end
177 && let Some(start) = open_trill_line.take() {
178 spans.push(SpanMark::TrillLine { start, end: addr.clone() });
179 }
180 }
181 }
182 }
183 }
184 }
185 spans
186}
187
188fn collect_beam_groups(score: &Score) -> Vec<BeamGroup> {
191 let mut groups = Vec::new();
192 for (pi, part) in score.parts.iter().enumerate() {
193 for (si, staff) in part.staves.iter().enumerate() {
194 for (mi, measure) in staff.measures.iter().enumerate() {
195 for vi in 0..4usize {
196 let voice = &measure.voices[vi];
197 let mut current: Option<Vec<usize>> = None;
198
199 for (ni, note) in voice.iter().enumerate() {
200 match note.beam {
201 BeamState::Begin => {
202 current = Some(vec![ni]);
203 }
204 BeamState::BeginEnd => {
205 groups.push(BeamGroup {
206 part: pi, staff: si, measure: mi, voice: vi,
207 note_indices: vec![ni],
208 });
209 }
210 BeamState::Continue
211 | BeamState::ForwardHook
212 | BeamState::BackwardHook => {
213 if let Some(ref mut g) = current {
214 g.push(ni);
215 }
216 }
217 BeamState::End => {
218 if let Some(mut g) = current.take() {
219 g.push(ni);
220 groups.push(BeamGroup {
221 part: pi, staff: si, measure: mi, voice: vi,
222 note_indices: g,
223 });
224 }
225 }
226 BeamState::None => {
227 current = None;
228 }
229 }
230 }
231 }
232 }
233 }
234 }
235 groups
236}
237
238fn collect_tuplet_groups(score: &Score) -> Vec<TupletGroup> {
241 let mut groups = Vec::new();
242 for (pi, part) in score.parts.iter().enumerate() {
243 for (si, staff) in part.staves.iter().enumerate() {
244 for (mi, measure) in staff.measures.iter().enumerate() {
245 for vi in 0..4usize {
246 let voice = &measure.voices[vi];
247 let mut current_indices: Vec<usize> = Vec::new();
248 let mut current_info: Option<TupletInfo> = None;
249
250 let flush = |indices: &mut Vec<usize>, info: &mut Option<TupletInfo>, groups: &mut Vec<TupletGroup>| {
251 if indices.len() >= 2
252 && let Some(ti) = info.take() {
253 groups.push(TupletGroup {
254 part: pi, staff: si, measure: mi, voice: vi,
255 note_indices: std::mem::take(indices),
256 actual_notes: ti.actual_notes,
257 normal_notes: ti.normal_notes,
258 });
259 return;
260 }
261 indices.clear();
262 info.take();
263 };
264
265 for (ni, note) in voice.iter().enumerate() {
266 match ¬e.tuplet {
267 Some(ti) => {
268 let same_group = current_info.as_ref()
269 .is_some_and(|prev| prev.actual_notes == ti.actual_notes && prev.normal_notes == ti.normal_notes);
270 if !same_group {
271 flush(&mut current_indices, &mut current_info, &mut groups);
272 current_info = Some(ti.clone());
273 }
274 current_indices.push(ni);
275 }
276 None => {
277 flush(&mut current_indices, &mut current_info, &mut groups);
278 }
279 }
280 }
281 flush(&mut current_indices, &mut current_info, &mut groups);
282 }
283 }
284 }
285 }
286 groups
287}
288
289fn step_idx(step: &Step) -> u8 {
290 match step {
291 Step::C => 0, Step::D => 1, Step::E => 2, Step::F => 3,
292 Step::G => 4, Step::A => 5, Step::B => 6,
293 }
294}
295
296fn key_alter(fifths: i8, step: &Step) -> i8 {
298 const SHARPS: [Step; 7] = [Step::F, Step::C, Step::G, Step::D, Step::A, Step::E, Step::B];
299 const FLATS: [Step; 7] = [Step::B, Step::E, Step::A, Step::D, Step::G, Step::C, Step::F];
300 if fifths > 0 && SHARPS[..fifths as usize].contains(step) { 1 }
301 else if fifths < 0 && FLATS[..(-fifths) as usize].contains(step) { -1 }
302 else { 0 }
303}
304
305fn collect_courtesy_accidentals(score: &Score) -> Vec<CourtesyAccidental> {
306 let mut result = Vec::new();
307 let base_fifths = score.settings.key_signature.fifths;
308
309 for (pi, part) in score.parts.iter().enumerate() {
310 for (si, staff) in part.staves.iter().enumerate() {
311 let mut prev_alters: HashMap<(u8, i8), i8> = HashMap::new();
313 let mut current_fifths = base_fifths;
314
315 for (mi, measure) in staff.measures.iter().enumerate() {
316 if let Some(key_sig) = measure.key_sig.as_ref() {
317 current_fifths = key_sig.fifths;
318 prev_alters.clear();
319 }
320
321 for vi in 0..4usize {
323 for (ni, note) in measure.voices[vi].iter().enumerate() {
324 if note.tie_end { continue; }
325 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
326 let key = (step_idx(&pitch.step), pitch.octave);
327 if prev_alters.contains_key(&key) {
328 result.push(CourtesyAccidental {
329 part: pi, staff: si, measure: mi, voice: vi,
330 note_index: ni, pitch_index: pitch_idx,
331 alter: pitch.alter,
332 });
333 }
334 }
335 }
336 }
337
338 prev_alters.clear();
340 for vi in 0..4usize {
341 for note in measure.voices[vi].iter() {
342 for pitch in note.pitches.iter() {
343 if pitch.alter != key_alter(current_fifths, &pitch.step) {
344 prev_alters.insert(
345 (step_idx(&pitch.step), pitch.octave),
346 pitch.alter,
347 );
348 }
349 }
350 }
351 }
352 }
353 }
354 }
355 result
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use acorde_core::{Clef, Measure, Part, Score, Staff};
362
363 fn score_with_measures(n: usize) -> Score {
364 let mut score = Score::default();
365 score.parts.clear();
366 let mut staff = Staff::new(Clef::Treble);
367 for i in 0..n {
368 let mut m = Measure::empty(4, 4);
369 m.number = i as u32 + 1;
370 staff.measures.push(m);
371 }
372 let mut part = Part::new("P1", "P1");
373 part.staves.push(staff);
374 score.parts.push(part);
375 score
376 }
377
378 #[test]
379 fn default_score_has_one_row_of_four() {
380 let score = Score::default();
382 let result = compute_layout(&score, &LayoutConfig::default());
383 assert_eq!(result.rows.len(), 1);
384 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
385 assert_eq!(result.vis_slots.len(), 4);
386 assert!(result.spans.is_empty());
387 }
388
389 #[test]
390 fn vis_slots_expand_multi_rest() {
391 let mut score = score_with_measures(1);
392 score.parts[0].staves[0].measures[0].multi_rest_count = Some(4);
393
394 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 8, concert_pitch: false, first_row_measures: None });
395 assert_eq!(result.vis_slots.len(), 4);
396 assert!(result.vis_slots.iter().all(|&v| v == 0));
397 }
398
399 #[test]
400 fn measures_split_into_rows() {
401 let score = score_with_measures(6);
402 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
403 assert_eq!(result.rows.len(), 2);
404 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
405 assert_eq!(result.rows[1].measure_indices, vec![4, 5]);
406 }
407
408 #[test]
409 fn single_measure_single_row() {
410 let score = score_with_measures(1);
411 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
412 assert_eq!(result.rows.len(), 1);
413 assert_eq!(result.rows[0].measure_indices, vec![0]);
414 }
415
416 #[test]
417 fn exactly_full_rows() {
418 let score = score_with_measures(8);
419 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
420 assert_eq!(result.rows.len(), 2);
421 assert_eq!(result.rows[0].measure_indices.len(), 4);
422 assert_eq!(result.rows[1].measure_indices.len(), 4);
423 }
424
425 #[test]
426 fn concert_pitch_bb_clarinet_transposes_key() {
427 let mut score = score_with_measures(1);
430 score.parts[0].staves[0].transpose_semitones = -2;
431 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: true, first_row_measures: None });
432 assert_eq!(result.concert_key_overrides.len(), 1);
433 assert_eq!(result.concert_key_overrides[0].part_index, 0);
434 assert_eq!(result.concert_key_overrides[0].staff_index, 0);
435 assert_eq!(result.concert_key_overrides[0].fifths, -2);
436 }
437
438 #[test]
439 fn concert_pitch_false_no_override() {
440 let mut score = score_with_measures(1);
441 score.parts[0].staves[0].transpose_semitones = -2;
442 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
443 assert!(result.concert_key_overrides.is_empty());
444 }
445
446 #[test]
447 fn concert_pitch_zero_transpose_no_override() {
448 let score = score_with_measures(1);
450 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: true, first_row_measures: None });
451 assert!(result.concert_key_overrides.is_empty());
452 }
453
454 #[test]
455 fn transpose_key_formula_eb_alto_sax() {
456 assert_eq!(transpose_key_fifths(0, -9), -3);
459 }
460
461 #[test]
462 fn transpose_key_formula_perfect_fifth_up() {
463 assert_eq!(transpose_key_fifths(0, 7), 1);
465 }
466
467 #[test]
468 fn system_break_splits_row() {
469 let mut score = score_with_measures(3);
471 score.parts[0].staves[0].measures[1].system_break = true;
472 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
473 assert_eq!(result.rows.len(), 2);
474 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
475 assert_eq!(result.rows[1].measure_indices, vec![2]);
476 }
477
478 #[test]
479 fn page_break_splits_row() {
480 let mut score = score_with_measures(3);
481 score.parts[0].staves[0].measures[0].page_break = true;
482 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
483 assert_eq!(result.rows.len(), 2);
484 assert_eq!(result.rows[0].measure_indices, vec![0]);
485 assert_eq!(result.rows[1].measure_indices, vec![1, 2]);
486 }
487
488 #[test]
489 fn system_break_overrides_per_row() {
490 let mut score = score_with_measures(5);
492 score.parts[0].staves[0].measures[1].system_break = true;
493 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
494 assert_eq!(result.rows.len(), 2);
495 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
496 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
497 }
498
499 #[test]
500 fn no_break_unchanged() {
501 let score = score_with_measures(3);
502 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
503 assert_eq!(result.rows.len(), 1);
504 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2]);
505 }
506
507 #[test]
510 fn first_row_measures_limits_first_row() {
511 let score = score_with_measures(5);
513 let result = compute_layout(&score, &LayoutConfig {
514 measures_per_row: 4,
515 concert_pitch: false,
516 first_row_measures: Some(2),
517 });
518 assert_eq!(result.rows.len(), 2);
519 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
520 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
521 }
522
523 #[test]
524 fn first_row_measures_none_unchanged() {
525 let score = score_with_measures(5);
527 let without = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
528 let with_none = compute_layout(&score, &LayoutConfig::default());
529 assert_eq!(without.rows.len(), with_none.rows.len());
530 for (a, b) in without.rows.iter().zip(with_none.rows.iter()) {
531 assert_eq!(a.measure_indices, b.measure_indices);
532 }
533 }
534
535 #[test]
536 fn beam_groups_empty_for_default_score() {
537 let score = Score::default();
538 let result = compute_layout(&score, &LayoutConfig::default());
539 assert!(result.beam_groups.is_empty());
540 }
541
542 #[test]
543 fn beam_groups_begin_end_collected() {
544 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
545 let mut score = score_with_measures(1);
546 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
547 n1.beam = BeamState::Begin;
548 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
549 n2.beam = BeamState::Continue;
550 let mut n3 = Note::new(Pitch::new(Step::E, 4), Duration::Eighth);
551 n3.beam = BeamState::End;
552 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2, n3];
553
554 let result = compute_layout(&score, &LayoutConfig::default());
555 assert_eq!(result.beam_groups.len(), 1);
556 assert_eq!(result.beam_groups[0].note_indices, vec![0, 1, 2]);
557 assert_eq!(result.beam_groups[0].part, 0);
558 assert_eq!(result.beam_groups[0].measure, 0);
559 assert_eq!(result.beam_groups[0].voice, 0);
560 }
561
562 #[test]
563 fn beam_groups_beginend_is_standalone_group() {
564 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
565 let mut score = score_with_measures(1);
566 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
567 n.beam = BeamState::BeginEnd;
568 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
569
570 let result = compute_layout(&score, &LayoutConfig::default());
571 assert_eq!(result.beam_groups.len(), 1);
572 assert_eq!(result.beam_groups[0].note_indices, vec![0]);
573 }
574
575 #[test]
576 fn tuplet_groups_empty_for_default_score() {
577 let score = Score::default();
578 let result = compute_layout(&score, &LayoutConfig::default());
579 assert!(result.tuplet_groups.is_empty());
580 }
581
582 #[test]
583 fn tuplet_groups_triplet_collected() {
584 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
585 let ti = TupletInfo { actual_notes: 3, normal_notes: 2 };
586 let mut score = score_with_measures(1);
587 let notes: Vec<Note> = (0..3).map(|i| {
588 let step = [Step::C, Step::D, Step::E][i].clone();
589 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
590 n.tuplet = Some(ti.clone());
591 n
592 }).collect();
593 score.parts[0].staves[0].measures[0].voices[0] = notes;
594
595 let result = compute_layout(&score, &LayoutConfig::default());
596 assert_eq!(result.tuplet_groups.len(), 1);
597 assert_eq!(result.tuplet_groups[0].note_indices, vec![0, 1, 2]);
598 assert_eq!(result.tuplet_groups[0].actual_notes, 3);
599 assert_eq!(result.tuplet_groups[0].normal_notes, 2);
600 assert_eq!(result.tuplet_groups[0].part, 0);
601 assert_eq!(result.tuplet_groups[0].measure, 0);
602 }
603
604 #[test]
605 fn tuplet_groups_no_group_for_single_tuplet_note() {
606 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
607 let mut score = score_with_measures(1);
608 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
609 n.tuplet = Some(TupletInfo { actual_notes: 3, normal_notes: 2 });
610 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
611
612 let result = compute_layout(&score, &LayoutConfig::default());
613 assert!(result.tuplet_groups.is_empty());
615 }
616
617 #[test]
618 fn hairpin_span_resolved() {
619 use acorde_core::{Duration, HairpinKind, Note, Pitch, Step};
620 let mut score = score_with_measures(1);
621 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
622 n1.hairpin_start = Some(HairpinKind::Crescendo);
623 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
624 n2.hairpin_end = true;
625 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
626
627 let result = compute_layout(&score, &LayoutConfig::default());
628 assert_eq!(result.spans.len(), 1);
629 if let SpanMark::Hairpin { kind, start, end } = &result.spans[0] {
630 assert_eq!(*kind, HairpinKind::Crescendo);
631 assert_eq!(start.note, 0);
632 assert_eq!(end.note, 1);
633 } else {
634 panic!("expected Hairpin span");
635 }
636 }
637
638 #[test]
639 fn slur_span_resolved() {
640 use acorde_core::{Duration, Note, Pitch, Step};
641 let mut score = score_with_measures(1);
642 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
643 n1.slur_start = true;
644 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
645 n2.slur_end = true;
646 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
647
648 let result = compute_layout(&score, &LayoutConfig::default());
649 let slur = result.spans.iter().find(|s| matches!(s, SpanMark::Slur { .. }));
650 assert!(slur.is_some(), "expected a Slur span");
651 if let Some(SpanMark::Slur { start, end }) = slur {
652 assert_eq!(start.note, 0);
653 assert_eq!(end.note, 1);
654 }
655 }
656
657 #[test]
660 fn courtesy_none_for_clean_score() {
661 let score = Score::default();
663 let result = compute_layout(&score, &LayoutConfig::default());
664 assert!(result.courtesy_accidentals.is_empty());
665 }
666
667 #[test]
668 fn courtesy_after_chromatic_alteration() {
669 use acorde_core::{Duration, Note, Pitch, Step};
672 let mut score = score_with_measures(2);
673
674 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
675 fsharp.pitches[0].alter = 1;
676 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
677
678 let fnat = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
679 score.parts[0].staves[0].measures[1].voices[0] = vec![fnat];
681
682 let result = compute_layout(&score, &LayoutConfig::default());
683 assert_eq!(result.courtesy_accidentals.len(), 1);
684 let ca = &result.courtesy_accidentals[0];
685 assert_eq!(ca.measure, 1);
686 assert_eq!(ca.alter, 0); }
688
689 #[test]
690 fn courtesy_same_alteration_no_courtesy() {
691 use acorde_core::{Duration, Note, Pitch, Step};
697 let mut score = score_with_measures(2);
698
699 let mut fsharp1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
700 fsharp1.pitches[0].alter = 1;
701 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp1];
702
703 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
704 fsharp2.pitches[0].alter = 1;
705 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
706
707 let result = compute_layout(&score, &LayoutConfig::default());
708 assert_eq!(result.courtesy_accidentals.len(), 1);
710 assert_eq!(result.courtesy_accidentals[0].alter, 1);
711 }
712
713 #[test]
714 fn courtesy_tied_note_excluded() {
715 use acorde_core::{Duration, Note, Pitch, Step};
717 let mut score = score_with_measures(2);
718
719 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
720 fsharp.pitches[0].alter = 1;
721 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
722
723 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
724 tied.pitches[0].alter = 1;
725 tied.tie_end = true;
726 score.parts[0].staves[0].measures[1].voices[0] = vec![tied];
727
728 let result = compute_layout(&score, &LayoutConfig::default());
729 assert!(result.courtesy_accidentals.is_empty());
730 }
731
732 #[test]
733 fn courtesy_key_change_resets() {
734 use acorde_core::{Duration, KeySignature, Note, Pitch, Step};
737 let mut score = score_with_measures(2);
738
739 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
740 fsharp.pitches[0].alter = 1;
741 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
742
743 score.parts[0].staves[0].measures[1].key_sig =
745 Some(KeySignature { fifths: 1, mode: "major".to_string() });
746 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
748 fsharp2.pitches[0].alter = 1;
749 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
750
751 let result = compute_layout(&score, &LayoutConfig::default());
752 assert!(result.courtesy_accidentals.is_empty());
754 }
755}