1use super::change_hint::{ChangeHint, ChangeScope};
2use super::commands::{
3 AddStaffCmd, Command, CommandStack, DeleteStaffCmd, PasteRangeCmd, PasteVoiceCmd,
4 RespellScoreCmd, RespellScoreToKeyCmd, SetArpeggioCmd, SetCueCmd, SetNoteHeadCmd,
5 SetPartGroupCmd, SetStemCmd, SetTupletCmd, ToggleSlurCmd, ToggleTrillLineCmd, command_hint,
6};
7use super::notation::{Clef, NoteHead, TupletInfo};
8use super::score::PartGroup;
9use super::score::{Note, NoteAddr, Score};
10use crate::Error;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Serialize, Deserialize)]
22pub struct EngineHistory {
23 pub initial_score: Score,
24 pub commands: Vec<Command>,
25}
26
27#[derive(Debug, Clone)]
28struct RangeClipboard {
29 voice: usize,
30 measures: Vec<Vec<Note>>,
31}
32
33pub struct ScoreEngine {
34 pub score: Score,
35 pub commands: CommandStack,
36 pub version: u64,
37 pub clipboard: Option<Vec<Note>>,
38 range_clipboard: Option<RangeClipboard>,
39 initial_score: Score,
40 pending_slur_start: Option<NoteAddr>,
41}
42
43impl Default for ScoreEngine {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl ScoreEngine {
50 pub fn new() -> Self {
51 let mut score = Score::default();
52 for part in &mut score.parts {
53 for staff in &mut part.staves {
54 for (i, m) in staff.measures.iter_mut().enumerate() {
55 m.number = i as u32 + 1;
56 }
57 }
58 }
59 let initial_score = score.clone();
60 Self {
61 score,
62 commands: CommandStack::new(200),
63 version: 0,
64 clipboard: None,
65 range_clipboard: None,
66 initial_score,
67 pending_slur_start: None,
68 }
69 }
70
71 pub fn apply(&mut self, cmd: Command) -> Result<ChangeHint, Error> {
72 let hint = command_hint(&cmd);
73 self.commands.execute(cmd, &mut self.score)?;
74 self.version += 1;
75 Ok(hint)
76 }
77
78 pub fn undo(&mut self) -> Result<ChangeHint, Error> {
79 let hint = self.commands.undo(&mut self.score)?;
80 self.version += 1;
81 Ok(hint)
82 }
83
84 pub fn redo(&mut self) -> Result<ChangeHint, Error> {
85 let hint = self.commands.redo(&mut self.score)?;
86 self.version += 1;
87 Ok(hint)
88 }
89
90 pub fn batch_apply(&mut self, cmds: Vec<Command>) -> Result<ChangeHint, Error> {
92 if cmds.is_empty() {
93 return Ok(ChangeHint {
94 scope: ChangeScope::Global,
95 layout_dirty: false,
96 playback_dirty: false,
97 });
98 }
99 let mut hint = command_hint(&cmds[0]);
100 for cmd in cmds.iter().skip(1) {
101 hint = hint.merge(command_hint(cmd));
102 }
103 self.commands.batch_execute(cmds, &mut self.score)?;
104 self.version += 1;
105 Ok(hint)
106 }
107
108 pub fn batch_apply_labeled(
112 &mut self,
113 cmds: Vec<Command>,
114 label: &str,
115 ) -> Result<ChangeHint, Error> {
116 if cmds.is_empty() {
117 return Ok(ChangeHint {
118 scope: ChangeScope::Global,
119 layout_dirty: false,
120 playback_dirty: false,
121 });
122 }
123 let mut hint = command_hint(&cmds[0]);
124 for cmd in cmds.iter().skip(1) {
125 hint = hint.merge(command_hint(cmd));
126 }
127 self.commands
128 .batch_execute_labeled(cmds, label.to_string(), &mut self.score)?;
129 self.version += 1;
130 Ok(hint)
131 }
132
133 pub fn undo_label(&self) -> Option<String> {
135 self.commands.undo_label()
136 }
137
138 pub fn redo_label(&self) -> Option<String> {
140 self.commands.redo_label()
141 }
142
143 pub fn undo_key(&self) -> Option<String> {
145 self.commands.undo_key()
146 }
147
148 pub fn redo_key(&self) -> Option<String> {
150 self.commands.redo_key()
151 }
152
153 pub fn replace_score(&mut self, score: Score) {
154 self.initial_score = score.clone();
155 self.score = score;
156 self.version += 1;
157 self.commands = CommandStack::new(200);
158 }
159
160 pub fn export_history(&self) -> EngineHistory {
165 EngineHistory {
166 initial_score: self.initial_score.clone(),
167 commands: self.commands.history_commands(),
168 }
169 }
170
171 pub fn from_history(history: EngineHistory) -> Result<Self, Error> {
176 let mut engine = ScoreEngine::new();
177 engine.replace_score(history.initial_score);
178 for cmd in history.commands {
179 engine.apply(cmd)?;
180 }
181 Ok(engine)
182 }
183
184 pub fn copy_voice(
185 &mut self,
186 part_index: usize,
187 staff_index: usize,
188 measure_index: usize,
189 voice_index: usize,
190 ) -> Result<(), Error> {
191 let voice = self
192 .score
193 .parts
194 .get(part_index)
195 .ok_or(Error::PartNotFound(part_index))?
196 .staves
197 .get(staff_index)
198 .ok_or(Error::StaffNotFound(staff_index))?
199 .measures
200 .get(measure_index)
201 .ok_or(Error::MeasureNotFound(measure_index))?
202 .voices
203 .get(voice_index)
204 .ok_or(Error::VoiceOutOfRange(voice_index))?;
205 self.clipboard = Some(voice.clone());
206 Ok(())
207 }
208
209 pub fn paste_voice(
210 &mut self,
211 part_index: usize,
212 staff_index: usize,
213 measure_index: usize,
214 voice_index: usize,
215 ) -> Result<ChangeHint, Error> {
216 let notes = self.clipboard.clone().ok_or(Error::ClipboardEmpty)?;
217 self.apply(Command::PasteVoice(PasteVoiceCmd {
218 part_index,
219 staff_index,
220 measure_index,
221 voice_index,
222 notes,
223 }))
224 }
225
226 pub fn copy_range(&mut self, start: NoteAddr, end: NoteAddr) -> Result<(), Error> {
231 if start.part != end.part || start.staff != end.staff || start.voice != end.voice {
232 return Err(Error::InvalidCommand(
233 "copy_range: start and end must share the same part, staff, and voice".into(),
234 ));
235 }
236 let from = start.measure.min(end.measure);
237 let to = start.measure.max(end.measure);
238 let staff = self
239 .score
240 .parts
241 .get(start.part)
242 .ok_or(Error::PartNotFound(start.part))?
243 .staves
244 .get(start.staff)
245 .ok_or(Error::StaffNotFound(start.staff))?;
246 if start.voice >= 4 {
247 return Err(Error::VoiceOutOfRange(start.voice));
248 }
249 let mut measures = Vec::new();
250 for mi in from..=to {
251 let m = staff.measures.get(mi).ok_or(Error::MeasureNotFound(mi))?;
252 measures.push(m.voices[start.voice].clone());
253 }
254 self.range_clipboard = Some(RangeClipboard {
255 voice: start.voice,
256 measures,
257 });
258 Ok(())
259 }
260
261 pub fn paste_range(&mut self, target: NoteAddr) -> Result<ChangeHint, Error> {
265 let rc = self.range_clipboard.clone().ok_or(Error::ClipboardEmpty)?;
266 self.apply(Command::PasteRange(PasteRangeCmd {
267 part_index: target.part,
268 staff_index: target.staff,
269 voice_index: rc.voice,
270 target_measure: target.measure,
271 measures: rc.measures,
272 }))
273 }
274
275 pub fn toggle_slur(&mut self, start: NoteAddr, end: NoteAddr) -> Result<ChangeHint, Error> {
277 self.apply(Command::ToggleSlur(ToggleSlurCmd { start, end }))
278 }
279
280 pub fn add_staff(&mut self, part_index: usize, clef: Clef) -> Result<ChangeHint, Error> {
282 self.apply(Command::AddStaff(AddStaffCmd { part_index, clef }))
283 }
284
285 pub fn delete_staff(
287 &mut self,
288 part_index: usize,
289 staff_index: usize,
290 ) -> Result<ChangeHint, Error> {
291 self.apply(Command::DeleteStaff(DeleteStaffCmd {
292 part_index,
293 staff_index,
294 }))
295 }
296
297 pub fn set_stem(&mut self, addr: NoteAddr, stem_up: Option<bool>) -> Result<ChangeHint, Error> {
301 self.apply(Command::SetStem(SetStemCmd {
302 part_index: addr.part,
303 staff_index: addr.staff,
304 measure_index: addr.measure,
305 voice_index: addr.voice,
306 note_index: addr.note,
307 stem_up,
308 }))
309 }
310
311 pub fn set_arpeggio(
313 &mut self,
314 addr: NoteAddr,
315 direction: Option<bool>,
316 ) -> Result<ChangeHint, Error> {
317 self.apply(Command::SetArpeggio(SetArpeggioCmd {
318 part_index: addr.part,
319 staff_index: addr.staff,
320 measure_index: addr.measure,
321 voice_index: addr.voice,
322 note_index: addr.note,
323 direction,
324 }))
325 }
326
327 pub fn set_note_head(
329 &mut self,
330 addr: NoteAddr,
331 note_head: NoteHead,
332 ) -> Result<ChangeHint, Error> {
333 self.apply(Command::SetNoteHead(SetNoteHeadCmd {
334 part_index: addr.part,
335 staff_index: addr.staff,
336 measure_index: addr.measure,
337 voice: addr.voice,
338 note_index: addr.note,
339 note_head,
340 }))
341 }
342
343 pub fn set_part_group(&mut self, group: Option<PartGroup>) -> Result<ChangeHint, Error> {
345 self.apply(Command::SetPartGroup(SetPartGroupCmd { group }))
346 }
347
348 pub fn toggle_trill_line(
350 &mut self,
351 start: NoteAddr,
352 end: NoteAddr,
353 ) -> Result<ChangeHint, Error> {
354 self.apply(Command::ToggleTrillLine(ToggleTrillLineCmd { start, end }))
355 }
356
357 pub fn set_cue(&mut self, addr: NoteAddr, is_cue: bool) -> Result<ChangeHint, Error> {
359 self.apply(Command::SetCue(SetCueCmd {
360 part_index: addr.part,
361 staff_index: addr.staff,
362 measure_index: addr.measure,
363 voice: addr.voice,
364 note_index: addr.note,
365 is_cue,
366 }))
367 }
368
369 pub fn set_tuplet(
371 &mut self,
372 addr: NoteAddr,
373 tuplet: Option<TupletInfo>,
374 ) -> Result<ChangeHint, Error> {
375 self.apply(Command::SetTuplet(SetTupletCmd {
376 part_index: addr.part,
377 staff_index: addr.staff,
378 measure_index: addr.measure,
379 voice_index: addr.voice,
380 note_index: addr.note,
381 tuplet,
382 }))
383 }
384
385 pub fn respell_score(&mut self, prefer_flat: bool) -> Result<ChangeHint, Error> {
387 self.apply(Command::RespellScore(RespellScoreCmd { prefer_flat }))
388 }
389
390 pub fn respell_score_to_key(&mut self) -> Result<ChangeHint, Error> {
392 self.apply(Command::RespellScoreToKey(RespellScoreToKeyCmd {}))
393 }
394
395 pub fn begin_slur(&mut self, start: NoteAddr) -> Result<(), Error> {
399 self.score
400 .parts
401 .get(start.part)
402 .ok_or(Error::PartNotFound(start.part))?
403 .staves
404 .get(start.staff)
405 .ok_or(Error::StaffNotFound(start.staff))?
406 .measures
407 .get(start.measure)
408 .ok_or(Error::MeasureNotFound(start.measure))?
409 .voices
410 .get(start.voice)
411 .ok_or(Error::VoiceOutOfRange(start.voice))?
412 .get(start.note)
413 .ok_or(Error::NoteNotFound(start.note))?;
414 self.pending_slur_start = Some(start);
415 Ok(())
416 }
417
418 pub fn end_slur(&mut self, end: NoteAddr) -> Result<ChangeHint, Error> {
422 let start = self
423 .pending_slur_start
424 .take()
425 .ok_or_else(|| Error::InvalidCommand("no slur in progress".to_string()))?;
426 self.apply(Command::ToggleSlur(ToggleSlurCmd { start, end }))
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use crate::model::commands::{NewScoreCmd, SetTempoCmd};
434
435 #[test]
436 fn new_engine_has_default_score() {
437 let engine = ScoreEngine::new();
438 assert_eq!(engine.version, 0);
439 assert_eq!(engine.score.parts.len(), 1);
440 }
441
442 #[test]
443 fn apply_increments_version() {
444 let mut engine = ScoreEngine::new();
445 engine
446 .apply(Command::SetTempo(SetTempoCmd { bpm: 140 }))
447 .unwrap();
448 assert_eq!(engine.version, 1);
449 }
450
451 #[test]
452 fn undo_redo_cycle() {
453 let mut engine = ScoreEngine::new();
454 engine
455 .apply(Command::SetTempo(SetTempoCmd { bpm: 140 }))
456 .unwrap();
457 let after_apply = engine.version;
458 engine.undo().unwrap();
459 assert_eq!(engine.score.settings.tempo_bpm, 120);
460 engine.redo().unwrap();
461 assert_eq!(engine.score.settings.tempo_bpm, 140);
462 assert!(engine.version > after_apply);
463 }
464
465 #[test]
466 fn replace_score_clears_history() {
467 let mut engine = ScoreEngine::new();
468 engine
469 .apply(Command::SetTempo(SetTempoCmd { bpm: 140 }))
470 .unwrap();
471 let new_score = Score::new("New", 90, 3, 4, 2, 8);
472 engine.replace_score(new_score);
473 assert!(engine.undo().is_err());
474 assert_eq!(engine.score.settings.tempo_bpm, 90);
475 }
476
477 #[test]
478 fn copy_paste_voice_copies_notes() {
479 use crate::model::duration::Duration;
480 use crate::model::pitch::{Pitch, Step};
481 use crate::model::score::Note;
482 let mut engine = ScoreEngine::new();
483 engine.score.parts[0].staves[0].measures[0].voices[0] =
484 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
485 engine.copy_voice(0, 0, 0, 0).unwrap();
486 engine.paste_voice(0, 0, 0, 1).unwrap();
487 let pasted = &engine.score.parts[0].staves[0].measures[0].voices[1];
488 assert_eq!(pasted.len(), 1);
489 assert_eq!(pasted[0].pitches[0].step, Step::C);
490 }
491
492 #[test]
493 fn paste_voice_undo_restores_original() {
494 use crate::model::duration::Duration;
495 use crate::model::pitch::{Pitch, Step};
496 use crate::model::score::Note;
497 let mut engine = ScoreEngine::new();
498 engine.score.parts[0].staves[0].measures[0].voices[0] =
499 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
500 engine.copy_voice(0, 0, 0, 0).unwrap();
501 engine.paste_voice(0, 0, 0, 1).unwrap();
502 engine.undo().unwrap();
503 assert!(engine.score.parts[0].staves[0].measures[0].voices[1].is_empty());
504 }
505
506 #[test]
507 fn paste_voice_without_copy_returns_error() {
508 let mut engine = ScoreEngine::new();
509 assert!(engine.paste_voice(0, 0, 0, 0).is_err());
510 }
511
512 #[test]
513 fn change_hint_set_tempo_is_global() {
514 use crate::model::change_hint::ChangeScope;
515 let mut engine = ScoreEngine::new();
516 let hint = engine
517 .apply(Command::SetTempo(SetTempoCmd { bpm: 100 }))
518 .unwrap();
519 assert_eq!(hint.scope, ChangeScope::Global);
520 assert!(!hint.layout_dirty);
521 assert!(hint.playback_dirty);
522 }
523
524 #[test]
525 fn change_hint_add_note_is_measure_scope() {
526 use crate::model::change_hint::ChangeScope;
527 use crate::model::commands::AddNoteCmd;
528 use crate::model::duration::Duration;
529 use crate::model::pitch::Pitch;
530 use crate::model::pitch::Step;
531 let mut engine = ScoreEngine::new();
532 let hint = engine
533 .apply(Command::AddNote(AddNoteCmd {
534 part_index: 0,
535 staff_index: 0,
536 measure_index: 0,
537 voice: 0,
538 position: 0,
539 pitch: Some(Pitch::new(Step::C, 4)),
540 duration: Duration::Quarter,
541 dot_count: 0,
542 is_rest: false,
543 tuplet: None,
544 }))
545 .unwrap();
546 assert_eq!(
547 hint.scope,
548 ChangeScope::Measures {
549 part: 0,
550 staff: 0,
551 start: 0,
552 end: 1
553 }
554 );
555 assert!(!hint.layout_dirty);
556 assert!(hint.playback_dirty);
557 }
558
559 #[test]
560 fn change_hint_set_part_name_no_dirty() {
561 use crate::model::change_hint::ChangeScope;
562 use crate::model::commands::SetPartNameCmd;
563 let mut engine = ScoreEngine::new();
564 let hint = engine
565 .apply(Command::SetPartName(SetPartNameCmd {
566 part_index: 0,
567 name: "Violin".into(),
568 short_name: "Vln.".into(),
569 }))
570 .unwrap();
571 assert_eq!(hint.scope, ChangeScope::Part(0));
572 assert!(!hint.layout_dirty);
573 assert!(!hint.playback_dirty);
574 }
575
576 #[test]
577 fn undo_returns_change_hint() {
578 use crate::model::change_hint::ChangeScope;
579 let mut engine = ScoreEngine::new();
580 engine
581 .apply(Command::SetTempo(SetTempoCmd { bpm: 160 }))
582 .unwrap();
583 let hint = engine.undo().unwrap();
584 assert_eq!(hint.scope, ChangeScope::Global);
585 assert!(hint.playback_dirty);
586 }
587
588 #[test]
589 fn redo_returns_change_hint() {
590 use crate::model::change_hint::ChangeScope;
591 let mut engine = ScoreEngine::new();
592 engine
593 .apply(Command::SetTempo(SetTempoCmd { bpm: 160 }))
594 .unwrap();
595 engine.undo().unwrap();
596 let hint = engine.redo().unwrap();
597 assert_eq!(hint.scope, ChangeScope::Global);
598 assert!(hint.playback_dirty);
599 }
600
601 #[test]
602 fn batch_apply_two_commands_single_undo() {
603 let mut engine = ScoreEngine::new();
604 let original = engine.score.settings.tempo_bpm;
605 engine
606 .batch_apply(vec![
607 Command::SetTempo(SetTempoCmd { bpm: 160 }),
608 Command::SetTempo(SetTempoCmd { bpm: 180 }),
609 ])
610 .unwrap();
611 assert_eq!(engine.score.settings.tempo_bpm, 180);
612 engine.undo().unwrap();
613 assert_eq!(engine.score.settings.tempo_bpm, original);
614 assert!(engine.undo().is_err());
615 }
616
617 #[test]
618 fn batch_apply_empty_returns_no_dirty() {
619 let mut engine = ScoreEngine::new();
620 let v0 = engine.version;
621 let hint = engine.batch_apply(vec![]).unwrap();
622 assert!(!hint.layout_dirty);
623 assert!(!hint.playback_dirty);
624 assert_eq!(engine.version, v0);
625 }
626
627 #[test]
628 fn batch_apply_hint_merges_scopes() {
629 use crate::model::change_hint::ChangeScope;
630 use crate::model::commands::{AddNoteCmd, SetTempoCmd};
631 use crate::model::duration::Duration;
632 use crate::model::pitch::{Pitch, Step};
633 let mut engine = ScoreEngine::new();
634 let hint = engine
635 .batch_apply(vec![
636 Command::SetTempo(SetTempoCmd { bpm: 140 }),
637 Command::AddNote(AddNoteCmd {
638 part_index: 0,
639 staff_index: 0,
640 measure_index: 0,
641 voice: 0,
642 position: 0,
643 pitch: Some(Pitch::new(Step::C, 4)),
644 duration: Duration::Quarter,
645 dot_count: 0,
646 is_rest: false,
647 tuplet: None,
648 }),
649 ])
650 .unwrap();
651 assert_eq!(hint.scope, ChangeScope::Global);
653 assert!(hint.playback_dirty);
654 }
655
656 #[test]
657 fn undo_label_none_when_empty() {
658 let engine = ScoreEngine::new();
659 assert!(engine.undo_label().is_none());
660 assert!(engine.redo_label().is_none());
661 }
662
663 #[test]
664 fn undo_label_after_command() {
665 let mut engine = ScoreEngine::new();
666 engine
667 .apply(Command::SetTempo(SetTempoCmd { bpm: 140 }))
668 .unwrap();
669 assert_eq!(engine.undo_label(), Some("Set Tempo".to_string()));
670 assert!(engine.redo_label().is_none());
671 }
672
673 #[test]
674 fn redo_label_after_undo() {
675 let mut engine = ScoreEngine::new();
676 engine
677 .apply(Command::SetTempo(SetTempoCmd { bpm: 140 }))
678 .unwrap();
679 engine.undo().unwrap();
680 assert!(engine.undo_label().is_none());
681 assert_eq!(engine.redo_label(), Some("Set Tempo".to_string()));
682 }
683
684 #[test]
685 fn copy_range_paste_range_roundtrip() {
686 use crate::model::duration::Duration;
687 use crate::model::pitch::{Pitch, Step};
688 use crate::model::score::{Note, NoteAddr};
689 let mut engine = ScoreEngine::new();
690 engine.score.parts[0].staves[0].measures[0].voices[0] =
692 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
693 use crate::model::commands::AddMeasureCmd;
695 engine
696 .apply(Command::AddMeasure(AddMeasureCmd { after_index: 0 }))
697 .unwrap();
698
699 let start = NoteAddr {
700 part: 0,
701 staff: 0,
702 measure: 0,
703 voice: 0,
704 note: 0,
705 };
706 let end = NoteAddr {
707 part: 0,
708 staff: 0,
709 measure: 0,
710 voice: 0,
711 note: 0,
712 };
713 engine.copy_range(start, end).unwrap();
714
715 let target = NoteAddr {
716 part: 0,
717 staff: 0,
718 measure: 1,
719 voice: 0,
720 note: 0,
721 };
722 engine.paste_range(target).unwrap();
723
724 let pasted = &engine.score.parts[0].staves[0].measures[1].voices[0];
725 assert_eq!(pasted.len(), 1);
726 assert_eq!(pasted[0].pitches[0].step, Step::C);
727 }
728
729 #[test]
730 fn paste_range_is_undoable() {
731 use crate::model::commands::AddMeasureCmd;
732 use crate::model::duration::Duration;
733 use crate::model::pitch::{Pitch, Step};
734 use crate::model::score::{Note, NoteAddr};
735 let mut engine = ScoreEngine::new();
736 engine.score.parts[0].staves[0].measures[0].voices[0] =
737 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
738 engine
739 .apply(Command::AddMeasure(AddMeasureCmd { after_index: 0 }))
740 .unwrap();
741
742 let start = NoteAddr {
743 part: 0,
744 staff: 0,
745 measure: 0,
746 voice: 0,
747 note: 0,
748 };
749 let end = start.clone();
750 engine.copy_range(start, end).unwrap();
751 let target = NoteAddr {
752 part: 0,
753 staff: 0,
754 measure: 1,
755 voice: 0,
756 note: 0,
757 };
758 engine.paste_range(target).unwrap();
759
760 engine.undo().unwrap();
762 let restored = &engine.score.parts[0].staves[0].measures[1].voices[0];
763 assert!(restored.iter().all(|n| n.is_rest));
764 }
765
766 #[test]
767 fn copy_range_mismatched_part_returns_error() {
768 let engine = ScoreEngine::new();
769 let mut e = ScoreEngine::new();
771 use crate::model::score::NoteAddr;
772 let start = NoteAddr {
773 part: 0,
774 staff: 0,
775 measure: 0,
776 voice: 0,
777 note: 0,
778 };
779 let end = NoteAddr {
780 part: 1,
781 staff: 0,
782 measure: 0,
783 voice: 0,
784 note: 0,
785 };
786 assert!(e.copy_range(start, end).is_err());
787 let _ = engine; }
789
790 #[test]
791 fn export_history_roundtrip() {
792 let mut engine = ScoreEngine::new();
793 engine
794 .apply(Command::SetTempo(SetTempoCmd { bpm: 160 }))
795 .unwrap();
796 engine
797 .apply(Command::SetTempo(SetTempoCmd { bpm: 180 }))
798 .unwrap();
799 let history = engine.export_history();
800 assert_eq!(history.commands.len(), 2);
801 let restored = ScoreEngine::from_history(history).unwrap();
802 assert_eq!(restored.score.settings.tempo_bpm, 180);
803 }
804
805 #[test]
806 fn export_history_empty_gives_initial_state() {
807 let engine = ScoreEngine::new();
808 let history = engine.export_history();
809 assert!(history.commands.is_empty());
810 let restored = ScoreEngine::from_history(history).unwrap();
811 assert_eq!(restored.score.settings.tempo_bpm, 120);
812 }
813
814 #[test]
815 fn replace_score_then_export_history() {
816 let mut engine = ScoreEngine::new();
817 let s = Score::new("Custom", 90, 3, 4, 2, 4);
818 engine.replace_score(s);
819 engine
820 .apply(Command::SetTempo(SetTempoCmd { bpm: 60 }))
821 .unwrap();
822 let history = engine.export_history();
823 assert_eq!(history.initial_score.settings.tempo_bpm, 90);
824 assert_eq!(history.commands.len(), 1);
825 let restored = ScoreEngine::from_history(history).unwrap();
826 assert_eq!(restored.score.settings.tempo_bpm, 60);
827 }
828
829 #[test]
830 fn new_score_command_replaces_score() {
831 let mut engine = ScoreEngine::new();
832 engine
833 .apply(Command::NewScore(NewScoreCmd {
834 title: "Sonata".into(),
835 composer: "Bach".into(),
836 tempo_bpm: 80,
837 time_numerator: 3,
838 time_denominator: 4,
839 key_fifths: -1,
840 measure_count: 12,
841 template: None,
842 }))
843 .unwrap();
844 assert_eq!(engine.score.metadata.title, "Sonata");
845 assert_eq!(engine.score.measure_count(), 12);
846 }
847
848 #[test]
849 fn respell_score_to_key_uses_key_signature() {
850 use crate::model::commands::{AddNoteCmd, RespellScoreToKeyCmd};
851 use crate::model::notation::KeySignature;
852 use crate::model::pitch::Step;
853 let mut engine = ScoreEngine::new();
854 engine.score.settings.key_signature = KeySignature {
856 fifths: -2,
857 mode: "major".to_string(),
858 };
859 engine
860 .apply(Command::AddNote(AddNoteCmd {
861 part_index: 0,
862 staff_index: 0,
863 measure_index: 0,
864 voice: 0,
865 position: 0,
866 pitch: Some(crate::model::pitch::Pitch::with_alter(Step::C, 4, 1)), duration: crate::model::duration::Duration::Quarter,
868 dot_count: 0,
869 is_rest: false,
870 tuplet: None,
871 }))
872 .unwrap();
873 engine
874 .apply(Command::RespellScoreToKey(RespellScoreToKeyCmd {}))
875 .unwrap();
876 let pitch = &engine.score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
877 assert_eq!(pitch.step, Step::D);
878 assert_eq!(pitch.alter, -1); }
880
881 #[test]
882 fn begin_end_slur_creates_slur() {
883 use crate::model::commands::AddNoteCmd;
884 use crate::model::duration::Duration;
885 use crate::model::pitch::{Pitch, Step};
886 let mut engine = ScoreEngine::new();
887 engine
888 .apply(Command::AddNote(AddNoteCmd {
889 part_index: 0,
890 staff_index: 0,
891 measure_index: 0,
892 voice: 0,
893 position: 0,
894 pitch: Some(Pitch::new(Step::C, 4)),
895 duration: Duration::Quarter,
896 dot_count: 0,
897 is_rest: false,
898 tuplet: None,
899 }))
900 .unwrap();
901 engine
902 .apply(Command::AddNote(AddNoteCmd {
903 part_index: 0,
904 staff_index: 0,
905 measure_index: 0,
906 voice: 0,
907 position: 1,
908 pitch: Some(Pitch::new(Step::D, 4)),
909 duration: Duration::Quarter,
910 dot_count: 0,
911 is_rest: false,
912 tuplet: None,
913 }))
914 .unwrap();
915 let start = NoteAddr {
916 part: 0,
917 staff: 0,
918 measure: 0,
919 voice: 0,
920 note: 0,
921 };
922 let end = NoteAddr {
923 part: 0,
924 staff: 0,
925 measure: 0,
926 voice: 0,
927 note: 1,
928 };
929 engine.begin_slur(start).unwrap();
930 engine.end_slur(end).unwrap();
931 assert!(engine.score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
932 assert!(engine.score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
933 }
934
935 #[test]
936 fn end_slur_without_begin_returns_error() {
937 let mut engine = ScoreEngine::new();
938 let end = NoteAddr {
939 part: 0,
940 staff: 0,
941 measure: 0,
942 voice: 0,
943 note: 0,
944 };
945 let result = engine.end_slur(end);
946 assert!(result.is_err());
947 }
948
949 #[test]
952 fn set_stem_sets_and_clears() {
953 use crate::model::commands::AddNoteCmd;
954 use crate::model::duration::Duration;
955 use crate::model::pitch::{Pitch, Step};
956 let mut engine = ScoreEngine::new();
957 engine
958 .apply(Command::AddNote(AddNoteCmd {
959 part_index: 0,
960 staff_index: 0,
961 measure_index: 0,
962 voice: 0,
963 position: 0,
964 pitch: Some(Pitch::new(Step::C, 4)),
965 duration: Duration::Quarter,
966 dot_count: 0,
967 is_rest: false,
968 tuplet: None,
969 }))
970 .unwrap();
971 let addr = NoteAddr {
972 part: 0,
973 staff: 0,
974 measure: 0,
975 voice: 0,
976 note: 0,
977 };
978 engine.set_stem(addr.clone(), Some(true)).unwrap();
979 assert_eq!(
980 engine.score.parts[0].staves[0].measures[0].voices[0][0].stem_up,
981 Some(true)
982 );
983 engine.set_stem(addr.clone(), Some(false)).unwrap();
984 assert_eq!(
985 engine.score.parts[0].staves[0].measures[0].voices[0][0].stem_up,
986 Some(false)
987 );
988 engine.set_stem(addr, None).unwrap();
989 assert_eq!(
990 engine.score.parts[0].staves[0].measures[0].voices[0][0].stem_up,
991 None
992 );
993 }
994
995 #[test]
996 fn set_stem_is_undoable() {
997 use crate::model::commands::AddNoteCmd;
998 use crate::model::duration::Duration;
999 use crate::model::pitch::{Pitch, Step};
1000 let mut engine = ScoreEngine::new();
1001 engine
1002 .apply(Command::AddNote(AddNoteCmd {
1003 part_index: 0,
1004 staff_index: 0,
1005 measure_index: 0,
1006 voice: 0,
1007 position: 0,
1008 pitch: Some(Pitch::new(Step::C, 4)),
1009 duration: Duration::Quarter,
1010 dot_count: 0,
1011 is_rest: false,
1012 tuplet: None,
1013 }))
1014 .unwrap();
1015 let addr = NoteAddr {
1016 part: 0,
1017 staff: 0,
1018 measure: 0,
1019 voice: 0,
1020 note: 0,
1021 };
1022 engine.set_stem(addr, Some(true)).unwrap();
1023 engine.undo().unwrap();
1024 assert_eq!(
1025 engine.score.parts[0].staves[0].measures[0].voices[0][0].stem_up,
1026 None
1027 );
1028 }
1029
1030 #[test]
1031 fn set_arpeggio_is_undoable() {
1032 use crate::model::commands::AddNoteCmd;
1033 use crate::model::duration::Duration;
1034 use crate::model::pitch::{Pitch, Step};
1035 let mut engine = ScoreEngine::new();
1036 engine
1037 .apply(Command::AddNote(AddNoteCmd {
1038 part_index: 0,
1039 staff_index: 0,
1040 measure_index: 0,
1041 voice: 0,
1042 position: 0,
1043 pitch: Some(Pitch::new(Step::C, 4)),
1044 duration: Duration::Quarter,
1045 dot_count: 0,
1046 is_rest: false,
1047 tuplet: None,
1048 }))
1049 .unwrap();
1050 let addr = NoteAddr {
1051 part: 0,
1052 staff: 0,
1053 measure: 0,
1054 voice: 0,
1055 note: 0,
1056 };
1057 engine.set_arpeggio(addr.clone(), Some(true)).unwrap();
1058 assert_eq!(
1059 engine.score.parts[0].staves[0].measures[0].voices[0][0].arpeggiate,
1060 Some(true)
1061 );
1062 engine.undo().unwrap();
1063 assert_eq!(
1064 engine.score.parts[0].staves[0].measures[0].voices[0][0].arpeggiate,
1065 None
1066 );
1067 engine.redo().unwrap();
1068 assert_eq!(
1069 engine.score.parts[0].staves[0].measures[0].voices[0][0].arpeggiate,
1070 Some(true)
1071 );
1072 }
1073
1074 #[test]
1077 fn undo_key_returns_key_string() {
1078 let mut engine = ScoreEngine::new();
1079 engine
1080 .apply(Command::SetTempo(SetTempoCmd { bpm: 140 }))
1081 .unwrap();
1082 assert_eq!(engine.undo_key(), Some("SetTempo".to_string()));
1083 assert!(engine.redo_key().is_none());
1084 }
1085
1086 #[test]
1087 fn redo_key_after_undo() {
1088 let mut engine = ScoreEngine::new();
1089 engine
1090 .apply(Command::SetTempo(SetTempoCmd { bpm: 140 }))
1091 .unwrap();
1092 engine.undo().unwrap();
1093 assert!(engine.undo_key().is_none());
1094 assert_eq!(engine.redo_key(), Some("SetTempo".to_string()));
1095 }
1096
1097 #[test]
1100 fn batch_apply_labeled_sets_undo_key() {
1101 let mut engine = ScoreEngine::new();
1102 engine
1103 .batch_apply_labeled(vec![Command::SetTempo(SetTempoCmd { bpm: 140 })], "ApplyAI")
1104 .unwrap();
1105 assert_eq!(engine.undo_key(), Some("ApplyAI".to_string()));
1106 }
1107
1108 #[test]
1109 fn batch_apply_labeled_empty_is_noop() {
1110 let mut engine = ScoreEngine::new();
1111 engine.batch_apply_labeled(vec![], "ApplyAI").unwrap();
1112 assert!(engine.undo_key().is_none());
1113 }
1114}