1use std::collections::HashMap;
2use acorde_core::{BeamState, HairpinKind, NoteAddr, OttavaKind, Score, Step, TupletInfo};
3use crate::{AccidentalMark, 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 let accidentals = collect_accidental_marks(score);
45
46 LayoutResult { vis_slots, rows, spans, concert_key_overrides, beam_groups, tuplet_groups, courtesy_accidentals, accidentals }
47}
48
49fn build_vis_slots(score: &Score) -> Vec<usize> {
52 let measure_count = score.parts.first()
53 .and_then(|p| p.staves.first())
54 .map(|s| s.measures.len())
55 .unwrap_or(0);
56
57 let mut slots = Vec::new();
58 for phys in 0..measure_count {
59 let count = multi_rest_count(score, phys).max(1);
60 for _ in 0..count {
61 slots.push(phys);
62 }
63 }
64 slots
65}
66
67fn multi_rest_count(score: &Score, measure_index: usize) -> usize {
68 score.parts.iter()
69 .flat_map(|p| p.staves.iter())
70 .filter_map(|s| s.measures.get(measure_index))
71 .filter_map(|m| m.multi_rest_count)
72 .map(|c| c as usize)
73 .max()
74 .unwrap_or(1)
75}
76
77fn force_break_after(score: &Score, phys: usize) -> bool {
80 score.parts.iter()
81 .flat_map(|p| p.staves.iter())
82 .filter_map(|s| s.measures.get(phys))
83 .any(|m| m.system_break || m.page_break)
84}
85
86fn build_rows(
87 score: &Score,
88 vis_slots: &[usize],
89 per_row: usize,
90 first_row_limit: Option<usize>,
91) -> Vec<RowLayout> {
92 if vis_slots.is_empty() {
93 return vec![RowLayout { measure_indices: Vec::new() }];
94 }
95
96 let mut rows: Vec<RowLayout> = Vec::new();
97 let mut current: Vec<usize> = Vec::new();
98 let mut last_phys = usize::MAX;
99
100 for &phys in vis_slots {
101 if phys != last_phys {
102 let limit = if rows.is_empty() {
103 first_row_limit.unwrap_or(per_row)
104 } else {
105 per_row
106 };
107 if current.len() == limit {
108 rows.push(RowLayout { measure_indices: std::mem::take(&mut current) });
109 }
110 current.push(phys);
111 if force_break_after(score, phys) {
112 rows.push(RowLayout { measure_indices: std::mem::take(&mut current) });
113 }
114 }
115 last_phys = phys;
116 }
117
118 if !current.is_empty() {
119 rows.push(RowLayout { measure_indices: current });
120 }
121 rows
122}
123
124fn resolve_spans(score: &Score) -> Vec<SpanMark> {
127 let mut spans = Vec::new();
128
129 for (pi, part) in score.parts.iter().enumerate() {
130 for (si, staff) in part.staves.iter().enumerate() {
131 for voice in 0..4usize {
132 let mut open_hairpin: Option<(NoteAddr, HairpinKind)> = None;
133 let mut open_ottava: Option<(NoteAddr, OttavaKind)> = None;
134 let mut open_pedal: Option<NoteAddr> = None;
135 let mut open_slur: Option<NoteAddr> = None;
136 let mut open_trill_line: Option<NoteAddr> = None;
137
138 for (mi, measure) in staff.measures.iter().enumerate() {
139 for (ni, note) in measure.voices[voice].iter().enumerate() {
140 let addr = NoteAddr { part: pi, staff: si, measure: mi, voice, note: ni };
141
142 if let Some(kind) = note.hairpin_start {
143 open_hairpin = Some((addr.clone(), kind));
144 }
145 if note.hairpin_end
146 && let Some((start, kind)) = open_hairpin.take() {
147 spans.push(SpanMark::Hairpin { kind, start, end: addr.clone() });
148 }
149
150 if let Some(kind) = note.ottava_start {
151 open_ottava = Some((addr.clone(), kind));
152 }
153 if note.ottava_end
154 && let Some((start, kind)) = open_ottava.take() {
155 spans.push(SpanMark::Ottava { kind, start, end: addr.clone() });
156 }
157
158 if note.pedal_start {
159 open_pedal = Some(addr.clone());
160 }
161 if note.pedal_end
162 && let Some(start) = open_pedal.take() {
163 spans.push(SpanMark::Pedal { start, end: addr.clone() });
164 }
165
166 if note.slur_start {
167 open_slur = Some(addr.clone());
168 }
169 if note.slur_end
170 && let Some(start) = open_slur.take() {
171 spans.push(SpanMark::Slur { start, end: addr.clone() });
172 }
173
174 if note.trill_line_start {
175 open_trill_line = Some(addr.clone());
176 }
177 if note.trill_line_end
178 && let Some(start) = open_trill_line.take() {
179 spans.push(SpanMark::TrillLine { start, end: addr.clone() });
180 }
181 }
182 }
183 }
184 }
185 }
186 spans
187}
188
189fn collect_beam_groups(score: &Score) -> Vec<BeamGroup> {
192 let mut groups = Vec::new();
193 for (pi, part) in score.parts.iter().enumerate() {
194 for (si, staff) in part.staves.iter().enumerate() {
195 for (mi, measure) in staff.measures.iter().enumerate() {
196 for vi in 0..4usize {
197 let voice = &measure.voices[vi];
198 let mut current: Option<Vec<usize>> = None;
199
200 for (ni, note) in voice.iter().enumerate() {
201 match note.beam {
202 BeamState::Begin => {
203 current = Some(vec![ni]);
204 }
205 BeamState::BeginEnd => {
206 groups.push(BeamGroup {
207 part: pi, staff: si, measure: mi, voice: vi,
208 note_indices: vec![ni],
209 });
210 }
211 BeamState::Continue
212 | BeamState::ForwardHook
213 | BeamState::BackwardHook => {
214 if let Some(ref mut g) = current {
215 g.push(ni);
216 }
217 }
218 BeamState::End => {
219 if let Some(mut g) = current.take() {
220 g.push(ni);
221 groups.push(BeamGroup {
222 part: pi, staff: si, measure: mi, voice: vi,
223 note_indices: g,
224 });
225 }
226 }
227 BeamState::None => {
228 current = None;
229 }
230 }
231 }
232 }
233 }
234 }
235 }
236 groups
237}
238
239fn collect_tuplet_groups(score: &Score) -> Vec<TupletGroup> {
242 let mut groups = Vec::new();
243 for (pi, part) in score.parts.iter().enumerate() {
244 for (si, staff) in part.staves.iter().enumerate() {
245 for (mi, measure) in staff.measures.iter().enumerate() {
246 for vi in 0..4usize {
247 let voice = &measure.voices[vi];
248 let mut current_indices: Vec<usize> = Vec::new();
249 let mut current_info: Option<TupletInfo> = None;
250
251 let flush = |indices: &mut Vec<usize>, info: &mut Option<TupletInfo>, groups: &mut Vec<TupletGroup>| {
252 if indices.len() >= 2
253 && let Some(ti) = info.take() {
254 groups.push(TupletGroup {
255 part: pi, staff: si, measure: mi, voice: vi,
256 note_indices: std::mem::take(indices),
257 actual_notes: ti.actual_notes,
258 normal_notes: ti.normal_notes,
259 });
260 return;
261 }
262 indices.clear();
263 info.take();
264 };
265
266 for (ni, note) in voice.iter().enumerate() {
267 match ¬e.tuplet {
268 Some(ti) => {
269 let same_group = current_info.as_ref()
270 .is_some_and(|prev| prev.actual_notes == ti.actual_notes && prev.normal_notes == ti.normal_notes);
271 if !same_group {
272 flush(&mut current_indices, &mut current_info, &mut groups);
273 current_info = Some(ti.clone());
274 }
275 current_indices.push(ni);
276 }
277 None => {
278 flush(&mut current_indices, &mut current_info, &mut groups);
279 }
280 }
281 }
282 flush(&mut current_indices, &mut current_info, &mut groups);
283 }
284 }
285 }
286 }
287 groups
288}
289
290fn step_idx(step: &Step) -> u8 {
291 match step {
292 Step::C => 0, Step::D => 1, Step::E => 2, Step::F => 3,
293 Step::G => 4, Step::A => 5, Step::B => 6,
294 }
295}
296
297fn key_alter(fifths: i8, step: &Step) -> i8 {
299 const SHARPS: [Step; 7] = [Step::F, Step::C, Step::G, Step::D, Step::A, Step::E, Step::B];
300 const FLATS: [Step; 7] = [Step::B, Step::E, Step::A, Step::D, Step::G, Step::C, Step::F];
301 if fifths > 0 && SHARPS[..fifths as usize].contains(step) { 1 }
302 else if fifths < 0 && FLATS[..(-fifths) as usize].contains(step) { -1 }
303 else { 0 }
304}
305
306fn collect_courtesy_accidentals(score: &Score) -> Vec<CourtesyAccidental> {
307 let mut result = Vec::new();
308 let base_fifths = score.settings.key_signature.fifths;
309
310 for (pi, part) in score.parts.iter().enumerate() {
311 for (si, staff) in part.staves.iter().enumerate() {
312 let mut prev_alters: HashMap<(u8, i8), i8> = HashMap::new();
314 let mut current_fifths = base_fifths;
315
316 for (mi, measure) in staff.measures.iter().enumerate() {
317 if let Some(key_sig) = measure.key_sig.as_ref() {
318 current_fifths = key_sig.fifths;
319 prev_alters.clear();
320 }
321
322 for vi in 0..4usize {
324 for (ni, note) in measure.voices[vi].iter().enumerate() {
325 if note.tie_end { continue; }
326 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
327 let key = (step_idx(&pitch.step), pitch.octave);
328 if prev_alters.contains_key(&key) {
329 result.push(CourtesyAccidental {
330 part: pi, staff: si, measure: mi, voice: vi,
331 note_index: ni, pitch_index: pitch_idx,
332 alter: pitch.alter,
333 });
334 }
335 }
336 }
337 }
338
339 prev_alters.clear();
341 for vi in 0..4usize {
342 for note in measure.voices[vi].iter() {
343 for pitch in note.pitches.iter() {
344 if pitch.alter != key_alter(current_fifths, &pitch.step) {
345 prev_alters.insert(
346 (step_idx(&pitch.step), pitch.octave),
347 pitch.alter,
348 );
349 }
350 }
351 }
352 }
353 }
354 }
355 }
356 result
357}
358
359fn collect_accidental_marks(score: &Score) -> Vec<AccidentalMark> {
368 let mut result = Vec::new();
369 let base_fifths = score.settings.key_signature.fifths;
370
371 for (pi, part) in score.parts.iter().enumerate() {
372 for (si, staff) in part.staves.iter().enumerate() {
373 let mut current_fifths = base_fifths;
374
375 for (mi, measure) in staff.measures.iter().enumerate() {
376 if let Some(key_sig) = measure.key_sig.as_ref() {
377 current_fifths = key_sig.fifths;
378 }
379
380 let mut active: HashMap<(u8, i8), i8> = HashMap::new();
382
383 for vi in 0..4usize {
384 for (ni, note) in measure.voices[vi].iter().enumerate() {
385 for (pitch_idx, pitch) in note.pitches.iter().enumerate() {
386 let key = (step_idx(&pitch.step), pitch.octave);
387 let baseline = active.get(&key).copied()
388 .unwrap_or_else(|| key_alter(current_fifths, &pitch.step));
389 if pitch.alter != baseline {
390 if !note.tie_end {
391 result.push(AccidentalMark {
392 part: pi, staff: si, measure: mi, voice: vi,
393 note_index: ni, pitch_index: pitch_idx,
394 alter: pitch.alter,
395 });
396 }
397 active.insert(key, pitch.alter);
398 }
399 }
400 }
401 }
402 }
403 }
404 }
405 result
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use acorde_core::{Clef, Measure, Part, Score, Staff};
412
413 fn score_with_measures(n: usize) -> Score {
414 let mut score = Score::default();
415 score.parts.clear();
416 let mut staff = Staff::new(Clef::Treble);
417 for i in 0..n {
418 let mut m = Measure::empty(4, 4);
419 m.number = i as u32 + 1;
420 staff.measures.push(m);
421 }
422 let mut part = Part::new("P1", "P1");
423 part.staves.push(staff);
424 score.parts.push(part);
425 score
426 }
427
428 #[test]
429 fn default_score_has_one_row_of_four() {
430 let score = Score::default();
432 let result = compute_layout(&score, &LayoutConfig::default());
433 assert_eq!(result.rows.len(), 1);
434 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
435 assert_eq!(result.vis_slots.len(), 4);
436 assert!(result.spans.is_empty());
437 }
438
439 #[test]
440 fn vis_slots_expand_multi_rest() {
441 let mut score = score_with_measures(1);
442 score.parts[0].staves[0].measures[0].multi_rest_count = Some(4);
443
444 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 8, concert_pitch: false, first_row_measures: None });
445 assert_eq!(result.vis_slots.len(), 4);
446 assert!(result.vis_slots.iter().all(|&v| v == 0));
447 }
448
449 #[test]
450 fn measures_split_into_rows() {
451 let score = score_with_measures(6);
452 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
453 assert_eq!(result.rows.len(), 2);
454 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2, 3]);
455 assert_eq!(result.rows[1].measure_indices, vec![4, 5]);
456 }
457
458 #[test]
459 fn single_measure_single_row() {
460 let score = score_with_measures(1);
461 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
462 assert_eq!(result.rows.len(), 1);
463 assert_eq!(result.rows[0].measure_indices, vec![0]);
464 }
465
466 #[test]
467 fn exactly_full_rows() {
468 let score = score_with_measures(8);
469 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
470 assert_eq!(result.rows.len(), 2);
471 assert_eq!(result.rows[0].measure_indices.len(), 4);
472 assert_eq!(result.rows[1].measure_indices.len(), 4);
473 }
474
475 #[test]
476 fn concert_pitch_bb_clarinet_transposes_key() {
477 let mut score = score_with_measures(1);
480 score.parts[0].staves[0].transpose_semitones = -2;
481 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: true, first_row_measures: None });
482 assert_eq!(result.concert_key_overrides.len(), 1);
483 assert_eq!(result.concert_key_overrides[0].part_index, 0);
484 assert_eq!(result.concert_key_overrides[0].staff_index, 0);
485 assert_eq!(result.concert_key_overrides[0].fifths, -2);
486 }
487
488 #[test]
489 fn concert_pitch_false_no_override() {
490 let mut score = score_with_measures(1);
491 score.parts[0].staves[0].transpose_semitones = -2;
492 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
493 assert!(result.concert_key_overrides.is_empty());
494 }
495
496 #[test]
497 fn concert_pitch_zero_transpose_no_override() {
498 let score = score_with_measures(1);
500 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: true, first_row_measures: None });
501 assert!(result.concert_key_overrides.is_empty());
502 }
503
504 #[test]
505 fn transpose_key_formula_eb_alto_sax() {
506 assert_eq!(transpose_key_fifths(0, -9), -3);
509 }
510
511 #[test]
512 fn transpose_key_formula_perfect_fifth_up() {
513 assert_eq!(transpose_key_fifths(0, 7), 1);
515 }
516
517 #[test]
518 fn system_break_splits_row() {
519 let mut score = score_with_measures(3);
521 score.parts[0].staves[0].measures[1].system_break = true;
522 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
523 assert_eq!(result.rows.len(), 2);
524 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
525 assert_eq!(result.rows[1].measure_indices, vec![2]);
526 }
527
528 #[test]
529 fn page_break_splits_row() {
530 let mut score = score_with_measures(3);
531 score.parts[0].staves[0].measures[0].page_break = true;
532 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
533 assert_eq!(result.rows.len(), 2);
534 assert_eq!(result.rows[0].measure_indices, vec![0]);
535 assert_eq!(result.rows[1].measure_indices, vec![1, 2]);
536 }
537
538 #[test]
539 fn system_break_overrides_per_row() {
540 let mut score = score_with_measures(5);
542 score.parts[0].staves[0].measures[1].system_break = true;
543 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
544 assert_eq!(result.rows.len(), 2);
545 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
546 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
547 }
548
549 #[test]
550 fn no_break_unchanged() {
551 let score = score_with_measures(3);
552 let result = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
553 assert_eq!(result.rows.len(), 1);
554 assert_eq!(result.rows[0].measure_indices, vec![0, 1, 2]);
555 }
556
557 #[test]
560 fn first_row_measures_limits_first_row() {
561 let score = score_with_measures(5);
563 let result = compute_layout(&score, &LayoutConfig {
564 measures_per_row: 4,
565 concert_pitch: false,
566 first_row_measures: Some(2),
567 });
568 assert_eq!(result.rows.len(), 2);
569 assert_eq!(result.rows[0].measure_indices, vec![0, 1]);
570 assert_eq!(result.rows[1].measure_indices, vec![2, 3, 4]);
571 }
572
573 #[test]
574 fn first_row_measures_none_unchanged() {
575 let score = score_with_measures(5);
577 let without = compute_layout(&score, &LayoutConfig { measures_per_row: 4, concert_pitch: false, first_row_measures: None });
578 let with_none = compute_layout(&score, &LayoutConfig::default());
579 assert_eq!(without.rows.len(), with_none.rows.len());
580 for (a, b) in without.rows.iter().zip(with_none.rows.iter()) {
581 assert_eq!(a.measure_indices, b.measure_indices);
582 }
583 }
584
585 #[test]
586 fn beam_groups_empty_for_default_score() {
587 let score = Score::default();
588 let result = compute_layout(&score, &LayoutConfig::default());
589 assert!(result.beam_groups.is_empty());
590 }
591
592 #[test]
593 fn beam_groups_begin_end_collected() {
594 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
595 let mut score = score_with_measures(1);
596 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
597 n1.beam = BeamState::Begin;
598 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
599 n2.beam = BeamState::Continue;
600 let mut n3 = Note::new(Pitch::new(Step::E, 4), Duration::Eighth);
601 n3.beam = BeamState::End;
602 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2, n3];
603
604 let result = compute_layout(&score, &LayoutConfig::default());
605 assert_eq!(result.beam_groups.len(), 1);
606 assert_eq!(result.beam_groups[0].note_indices, vec![0, 1, 2]);
607 assert_eq!(result.beam_groups[0].part, 0);
608 assert_eq!(result.beam_groups[0].measure, 0);
609 assert_eq!(result.beam_groups[0].voice, 0);
610 }
611
612 #[test]
613 fn beam_groups_beginend_is_standalone_group() {
614 use acorde_core::{BeamState, Duration, Note, Pitch, Step};
615 let mut score = score_with_measures(1);
616 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
617 n.beam = BeamState::BeginEnd;
618 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
619
620 let result = compute_layout(&score, &LayoutConfig::default());
621 assert_eq!(result.beam_groups.len(), 1);
622 assert_eq!(result.beam_groups[0].note_indices, vec![0]);
623 }
624
625 #[test]
626 fn tuplet_groups_empty_for_default_score() {
627 let score = Score::default();
628 let result = compute_layout(&score, &LayoutConfig::default());
629 assert!(result.tuplet_groups.is_empty());
630 }
631
632 #[test]
633 fn tuplet_groups_triplet_collected() {
634 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
635 let ti = TupletInfo { actual_notes: 3, normal_notes: 2 };
636 let mut score = score_with_measures(1);
637 let notes: Vec<Note> = (0..3).map(|i| {
638 let step = [Step::C, Step::D, Step::E][i].clone();
639 let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
640 n.tuplet = Some(ti.clone());
641 n
642 }).collect();
643 score.parts[0].staves[0].measures[0].voices[0] = notes;
644
645 let result = compute_layout(&score, &LayoutConfig::default());
646 assert_eq!(result.tuplet_groups.len(), 1);
647 assert_eq!(result.tuplet_groups[0].note_indices, vec![0, 1, 2]);
648 assert_eq!(result.tuplet_groups[0].actual_notes, 3);
649 assert_eq!(result.tuplet_groups[0].normal_notes, 2);
650 assert_eq!(result.tuplet_groups[0].part, 0);
651 assert_eq!(result.tuplet_groups[0].measure, 0);
652 }
653
654 #[test]
655 fn tuplet_groups_no_group_for_single_tuplet_note() {
656 use acorde_core::{Duration, Note, Pitch, Step, TupletInfo};
657 let mut score = score_with_measures(1);
658 let mut n = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
659 n.tuplet = Some(TupletInfo { actual_notes: 3, normal_notes: 2 });
660 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
661
662 let result = compute_layout(&score, &LayoutConfig::default());
663 assert!(result.tuplet_groups.is_empty());
665 }
666
667 #[test]
668 fn hairpin_span_resolved() {
669 use acorde_core::{Duration, HairpinKind, Note, Pitch, Step};
670 let mut score = score_with_measures(1);
671 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
672 n1.hairpin_start = Some(HairpinKind::Crescendo);
673 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
674 n2.hairpin_end = true;
675 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
676
677 let result = compute_layout(&score, &LayoutConfig::default());
678 assert_eq!(result.spans.len(), 1);
679 if let SpanMark::Hairpin { kind, start, end } = &result.spans[0] {
680 assert_eq!(*kind, HairpinKind::Crescendo);
681 assert_eq!(start.note, 0);
682 assert_eq!(end.note, 1);
683 } else {
684 panic!("expected Hairpin span");
685 }
686 }
687
688 #[test]
689 fn slur_span_resolved() {
690 use acorde_core::{Duration, Note, Pitch, Step};
691 let mut score = score_with_measures(1);
692 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
693 n1.slur_start = true;
694 let mut n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
695 n2.slur_end = true;
696 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
697
698 let result = compute_layout(&score, &LayoutConfig::default());
699 let slur = result.spans.iter().find(|s| matches!(s, SpanMark::Slur { .. }));
700 assert!(slur.is_some(), "expected a Slur span");
701 if let Some(SpanMark::Slur { start, end }) = slur {
702 assert_eq!(start.note, 0);
703 assert_eq!(end.note, 1);
704 }
705 }
706
707 #[test]
710 fn courtesy_none_for_clean_score() {
711 let score = Score::default();
713 let result = compute_layout(&score, &LayoutConfig::default());
714 assert!(result.courtesy_accidentals.is_empty());
715 }
716
717 #[test]
718 fn courtesy_after_chromatic_alteration() {
719 use acorde_core::{Duration, Note, Pitch, Step};
722 let mut score = score_with_measures(2);
723
724 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
725 fsharp.pitches[0].alter = 1;
726 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
727
728 let fnat = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
729 score.parts[0].staves[0].measures[1].voices[0] = vec![fnat];
731
732 let result = compute_layout(&score, &LayoutConfig::default());
733 assert_eq!(result.courtesy_accidentals.len(), 1);
734 let ca = &result.courtesy_accidentals[0];
735 assert_eq!(ca.measure, 1);
736 assert_eq!(ca.alter, 0); }
738
739 #[test]
740 fn courtesy_same_alteration_no_courtesy() {
741 use acorde_core::{Duration, Note, Pitch, Step};
747 let mut score = score_with_measures(2);
748
749 let mut fsharp1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
750 fsharp1.pitches[0].alter = 1;
751 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp1];
752
753 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
754 fsharp2.pitches[0].alter = 1;
755 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
756
757 let result = compute_layout(&score, &LayoutConfig::default());
758 assert_eq!(result.courtesy_accidentals.len(), 1);
760 assert_eq!(result.courtesy_accidentals[0].alter, 1);
761 }
762
763 #[test]
764 fn courtesy_tied_note_excluded() {
765 use acorde_core::{Duration, Note, Pitch, Step};
767 let mut score = score_with_measures(2);
768
769 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
770 fsharp.pitches[0].alter = 1;
771 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
772
773 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
774 tied.pitches[0].alter = 1;
775 tied.tie_end = true;
776 score.parts[0].staves[0].measures[1].voices[0] = vec![tied];
777
778 let result = compute_layout(&score, &LayoutConfig::default());
779 assert!(result.courtesy_accidentals.is_empty());
780 }
781
782 #[test]
783 fn courtesy_key_change_resets() {
784 use acorde_core::{Duration, KeySignature, Note, Pitch, Step};
787 let mut score = score_with_measures(2);
788
789 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
790 fsharp.pitches[0].alter = 1;
791 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
792
793 score.parts[0].staves[0].measures[1].key_sig =
795 Some(KeySignature { fifths: 1, mode: "major".to_string() });
796 let mut fsharp2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
798 fsharp2.pitches[0].alter = 1;
799 score.parts[0].staves[0].measures[1].voices[0] = vec![fsharp2];
800
801 let result = compute_layout(&score, &LayoutConfig::default());
802 assert!(result.courtesy_accidentals.is_empty());
804 }
805
806 #[test]
809 fn accidental_none_for_clean_score() {
810 let score = Score::default();
811 let result = compute_layout(&score, &LayoutConfig::default());
812 assert!(result.accidentals.is_empty());
813 }
814
815 #[test]
816 fn accidental_first_chromatic_note_in_measure() {
817 use acorde_core::{Duration, Note, Pitch, Step};
819 let mut score = score_with_measures(1);
820 let mut fsharp = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
821 fsharp.pitches[0].alter = 1;
822 score.parts[0].staves[0].measures[0].voices[0] = vec![fsharp];
823
824 let result = compute_layout(&score, &LayoutConfig::default());
825 assert_eq!(result.accidentals.len(), 1);
826 assert_eq!(result.accidentals[0].alter, 1);
827 assert_eq!(result.accidentals[0].measure, 0);
828 }
829
830 #[test]
831 fn accidental_repeat_same_alter_in_measure_not_marked_again() {
832 use acorde_core::{Duration, Note, Pitch, Step};
834 let mut score = score_with_measures(1);
835 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
836 f1.pitches[0].alter = 1;
837 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
838 f2.pitches[0].alter = 1;
839 score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
840
841 let result = compute_layout(&score, &LayoutConfig::default());
842 assert_eq!(result.accidentals.len(), 1);
843 }
844
845 #[test]
846 fn accidental_reverts_to_natural_within_same_measure() {
847 use acorde_core::{Duration, Note, Pitch, Step};
849 let mut score = score_with_measures(1);
850 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
851 f1.pitches[0].alter = 1;
852 let f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter); score.parts[0].staves[0].measures[0].voices[0] = vec![f1, f2];
854
855 let result = compute_layout(&score, &LayoutConfig::default());
856 assert_eq!(result.accidentals.len(), 2);
857 assert_eq!(result.accidentals[0].alter, 1);
858 assert_eq!(result.accidentals[1].alter, 0);
859 }
860
861 #[test]
862 fn accidental_does_not_carry_across_barline() {
863 use acorde_core::{Duration, Note, Pitch, Step};
867 let mut score = score_with_measures(2);
868 let mut f1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
869 f1.pitches[0].alter = 1;
870 score.parts[0].staves[0].measures[0].voices[0] = vec![f1];
871 let mut f2 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
872 f2.pitches[0].alter = 1;
873 score.parts[0].staves[0].measures[1].voices[0] = vec![f2];
874
875 let result = compute_layout(&score, &LayoutConfig::default());
876 assert_eq!(result.accidentals.len(), 2);
877 assert!(result.accidentals.iter().any(|a| a.measure == 1));
878 assert_eq!(result.courtesy_accidentals.len(), 1);
880 assert_eq!(result.courtesy_accidentals[0].measure, 1);
881 }
882
883 #[test]
884 fn accidental_tied_note_excluded() {
885 use acorde_core::{Duration, Note, Pitch, Step};
886 let mut score = score_with_measures(1);
887 let mut tied = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
888 tied.pitches[0].alter = 1;
889 tied.tie_end = true;
890 score.parts[0].staves[0].measures[0].voices[0] = vec![tied];
891
892 let result = compute_layout(&score, &LayoutConfig::default());
893 assert!(result.accidentals.is_empty());
894 }
895
896 #[test]
897 fn accidental_shared_across_voices_on_same_staff() {
898 use acorde_core::{Duration, Note, Pitch, Step};
900 let mut score = score_with_measures(1);
901 let mut f_v0 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
902 f_v0.pitches[0].alter = 1;
903 score.parts[0].staves[0].measures[0].voices[0] = vec![f_v0];
904 let mut f_v1 = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
905 f_v1.pitches[0].alter = 1;
906 score.parts[0].staves[0].measures[0].voices[1] = vec![f_v1];
907
908 let result = compute_layout(&score, &LayoutConfig::default());
909 assert_eq!(result.accidentals.len(), 1);
910 }
911
912 #[test]
913 fn accidental_double_sharp_marked() {
914 use acorde_core::{Duration, Note, Pitch, Step};
915 let mut score = score_with_measures(1);
916 let mut n = Note::new(Pitch::new(Step::F, 4), Duration::Quarter);
917 n.pitches[0].alter = 2;
918 score.parts[0].staves[0].measures[0].voices[0] = vec![n];
919
920 let result = compute_layout(&score, &LayoutConfig::default());
921 assert_eq!(result.accidentals.len(), 1);
922 assert_eq!(result.accidentals[0].alter, 2);
923 }
924}