1use super::change_hint::{ChangeHint, ChangeScope};
2use super::duration::Duration;
3use super::notation::{
4 Articulation, Barline, ChordSymbol, Clef, Dynamic, GuitarTechnique, HairpinKind, KeySignature,
5 Lyric, NoteHead, OttavaKind, TimeSignature, TupletInfo,
6};
7use super::pitch::Pitch;
8use super::score::{
9 Measure, Note, NoteAddr, Part, PartGroup, Score, ScoreTemplate, Staff, respell_score,
10 respell_score_to_key,
11};
12use crate::Error;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "snake_case")]
17pub enum Command {
18 AddNote(AddNoteCmd),
19 AddPitch(AddPitchCmd),
20 DeleteNote(DeleteNoteCmd),
21 AddMeasure(AddMeasureCmd),
22 DeleteMeasure(DeleteMeasureCmd),
23 SetTempo(SetTempoCmd),
24 NewScore(NewScoreCmd),
25 AddHairpin(AddHairpinCmd),
26 ToggleTie(ToggleTieCmd),
27 SetDynamic(SetDynamicCmd),
28 ToggleArticulation(ToggleArticulationCmd),
29 SetKeySignature(SetKeySignatureCmd),
30 SetTimeSignature(SetTimeSignatureCmd),
31 SetBarline(SetBarlineCmd),
32 AddPart(AddPartCmd),
33 DeletePart(DeletePartCmd),
34 SetMetadata(SetMetadataCmd),
35 SetRehearsalMark(SetRehearsalMarkCmd),
36 SetNavigationMark(SetNavigationMarkCmd),
37 SetChordSymbol(SetChordSymbolCmd),
38 SetGrace(SetGraceCmd),
39 SetOttava(SetOttavaCmd),
40 SetLyric(SetLyricCmd),
41 SetMultiRest(SetMultiRestCmd),
42 AddPedal(AddPedalCmd),
43 SetVolta(SetVoltaCmd),
44 SetClef(SetClefCmd),
45 SetPartName(SetPartNameCmd),
46 SetMidiInstrument(SetMidiInstrumentCmd),
47 SetTranspose(SetTransposeCmd),
48 SetTempoAtMeasure(SetTempoAtMeasureCmd),
49 PasteVoice(PasteVoiceCmd),
50 PasteRange(PasteRangeCmd),
51 SetSystemBreak(SetSystemBreakCmd),
52 SetPageBreak(SetPageBreakCmd),
53 ToggleSlur(ToggleSlurCmd),
54 AddStaff(AddStaffCmd),
55 DeleteStaff(DeleteStaffCmd),
56 SetTuplet(SetTupletCmd),
57 RespellScore(RespellScoreCmd),
58 RespellScoreToKey(RespellScoreToKeyCmd),
59 SetStem(SetStemCmd),
60 SetArpeggio(SetArpeggioCmd),
61 SetTechniqueText(SetTechniqueTextCmd),
62 SetFingering(SetFingeringCmd),
63 SetStringNumber(SetStringNumberCmd),
64 SetNoteHead(SetNoteHeadCmd),
65 SetCue(SetCueCmd),
66 SetGuitarTechnique(SetGuitarTechniqueCmd),
67 SetExpressionText(SetExpressionTextCmd),
68 ToggleTrillLine(ToggleTrillLineCmd),
69 SetPartGroup(SetPartGroupCmd),
70 Batch(BatchCmd),
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct AddNoteCmd {
75 pub part_index: usize,
76 pub staff_index: usize,
77 pub measure_index: usize,
78 pub voice: usize,
79 pub position: usize,
80 pub pitch: Option<Pitch>,
81 pub duration: Duration,
82 pub dot_count: u8,
83 pub is_rest: bool,
84 #[serde(default)]
85 pub tuplet: Option<TupletInfo>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct AddPitchCmd {
90 pub part_index: usize,
91 pub staff_index: usize,
92 pub measure_index: usize,
93 pub voice: usize,
94 pub note_index: usize,
95 pub pitch: Pitch,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct DeleteNoteCmd {
100 pub note_id: String,
101 pub part_index: usize,
102 pub staff_index: usize,
103 pub measure_index: usize,
104 pub voice: usize,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct AddMeasureCmd {
109 pub after_index: usize,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct DeleteMeasureCmd {
114 pub measure_index: usize,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct SetTempoCmd {
119 pub bpm: u16,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct NewScoreCmd {
124 pub title: String,
125 pub composer: String,
126 pub tempo_bpm: u16,
127 pub time_numerator: u8,
128 pub time_denominator: u8,
129 pub key_fifths: i8,
130 pub measure_count: u32,
131 #[serde(default)]
133 pub template: Option<ScoreTemplate>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct AddHairpinCmd {
138 pub part_index: usize,
139 pub staff_index: usize,
140 pub measure_index: usize,
141 pub voice: usize,
142 pub start_note_idx: usize,
143 pub end_note_idx: usize,
144 pub kind: HairpinKind,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ToggleTieCmd {
149 pub part_index: usize,
150 pub staff_index: usize,
151 pub measure_index: usize,
152 pub voice: usize,
153 pub note_index: usize,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SetDynamicCmd {
158 pub part_index: usize,
159 pub staff_index: usize,
160 pub measure_index: usize,
161 pub voice: usize,
162 pub note_index: usize,
163 pub dynamic: Option<Dynamic>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct ToggleArticulationCmd {
168 pub part_index: usize,
169 pub staff_index: usize,
170 pub measure_index: usize,
171 pub voice: usize,
172 pub note_index: usize,
173 pub articulation: Articulation,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct SetKeySignatureCmd {
178 pub fifths: i8,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct SetTimeSignatureCmd {
183 pub numerator: u8,
184 pub denominator: u8,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct SetBarlineCmd {
189 pub measure_index: usize,
190 pub side: String,
192 pub barline: Barline,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct AddPartCmd {
197 pub name: String,
198 pub short_name: String,
199 pub clefs: Vec<String>,
201 #[serde(default)]
203 pub midi_channel: u8,
204 #[serde(default)]
206 pub midi_program: u8,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct DeletePartCmd {
211 pub part_index: usize,
212}
213
214#[derive(Debug, Clone, Default, Serialize, Deserialize)]
215pub struct SetMetadataCmd {
216 pub title: Option<String>,
217 pub composer: Option<String>,
218 pub lyricist: Option<String>,
219 pub copyright: Option<String>,
220 pub work_number: Option<String>,
221 pub movement_title: Option<String>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct SetRehearsalMarkCmd {
226 pub measure_index: usize,
227 pub text: Option<String>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct SetNavigationMarkCmd {
232 pub measure_index: usize,
233 pub mark: Option<String>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct SetChordSymbolCmd {
240 pub part_index: usize,
241 pub staff_index: usize,
242 pub measure_index: usize,
243 pub voice: usize,
244 pub note_index: usize,
245 pub chord: Option<ChordSymbol>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct SetGraceCmd {
250 pub part_index: usize,
251 pub staff_index: usize,
252 pub measure_index: usize,
253 pub voice: usize,
254 pub note_index: usize,
255 pub is_grace: bool,
256 pub slash: bool,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct SetOttavaCmd {
262 pub part_index: usize,
263 pub staff_index: usize,
264 pub measure_index: usize,
265 pub voice: usize,
266 pub note_index: usize,
267 pub ottava_start: Option<OttavaKind>,
268 pub ottava_end: bool,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct SetLyricCmd {
273 pub part_index: usize,
274 pub staff_index: usize,
275 pub measure_index: usize,
276 pub voice: usize,
277 pub note_index: usize,
278 pub lyric: Option<Lyric>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct SetMultiRestCmd {
283 pub measure_index: usize,
284 pub count: Option<u8>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct SetVoltaCmd {
289 pub measure_index: usize,
290 pub volta: Option<super::score::VoltaBracket>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct SetClefCmd {
295 pub part_index: usize,
296 pub staff_index: usize,
297 pub clef: Clef,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct SetPartNameCmd {
302 pub part_index: usize,
303 pub name: String,
304 pub short_name: String,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct SetMidiInstrumentCmd {
309 pub part_index: usize,
310 pub midi_channel: u8,
312 pub midi_program: u8,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct SetTransposeCmd {
318 pub part_index: usize,
319 pub staff_index: usize,
320 pub semitones: i8,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct SetTempoAtMeasureCmd {
326 pub measure_index: usize,
327 pub bpm: Option<u16>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct PasteVoiceCmd {
333 pub part_index: usize,
334 pub staff_index: usize,
335 pub measure_index: usize,
336 pub voice_index: usize,
337 pub notes: Vec<Note>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct SetSystemBreakCmd {
343 pub measure_index: usize,
344 pub value: bool,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct SetPageBreakCmd {
349 pub measure_index: usize,
350 pub value: bool,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct BatchCmd {
356 pub commands: Vec<Command>,
357 #[serde(default)]
360 pub label: Option<String>,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct AddPedalCmd {
365 pub part_index: usize,
366 pub staff_index: usize,
367 pub measure_index: usize,
368 pub voice: usize,
369 pub start_note_idx: usize,
370 pub end_note_idx: usize,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct PasteRangeCmd {
379 pub part_index: usize,
380 pub staff_index: usize,
381 pub voice_index: usize,
382 pub target_measure: usize,
383 pub measures: Vec<Vec<Note>>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct ToggleSlurCmd {
390 pub start: NoteAddr,
391 pub end: NoteAddr,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct SetPartGroupCmd {
397 pub group: Option<PartGroup>,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct ToggleTrillLineCmd {
404 pub start: NoteAddr,
405 pub end: NoteAddr,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct AddStaffCmd {
414 pub part_index: usize,
415 pub clef: Clef,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct DeleteStaffCmd {
421 pub part_index: usize,
422 pub staff_index: usize,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct SetTupletCmd {
428 pub part_index: usize,
429 pub staff_index: usize,
430 pub measure_index: usize,
431 pub voice_index: usize,
432 pub note_index: usize,
433 pub tuplet: Option<TupletInfo>,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct SetStemCmd {
440 pub part_index: usize,
441 pub staff_index: usize,
442 pub measure_index: usize,
443 pub voice_index: usize,
444 pub note_index: usize,
445 pub stem_up: Option<bool>,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct SetArpeggioCmd {
451 pub part_index: usize,
452 pub staff_index: usize,
453 pub measure_index: usize,
454 pub voice_index: usize,
455 pub note_index: usize,
456 pub direction: Option<bool>,
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct SetTechniqueTextCmd {
463 pub part_index: usize,
464 pub staff_index: usize,
465 pub measure_index: usize,
466 pub voice: usize,
467 pub note_index: usize,
468 pub text: Option<String>,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct SetFingeringCmd {
475 pub part_index: usize,
476 pub staff_index: usize,
477 pub measure_index: usize,
478 pub voice: usize,
479 pub note_index: usize,
480 pub fingering: Option<u8>,
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct SetStringNumberCmd {
487 pub part_index: usize,
488 pub staff_index: usize,
489 pub measure_index: usize,
490 pub voice: usize,
491 pub note_index: usize,
492 pub string_number: Option<u8>,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct SetGuitarTechniqueCmd {
499 pub part_index: usize,
500 pub staff_index: usize,
501 pub measure_index: usize,
502 pub voice: usize,
503 pub note_index: usize,
504 pub technique: Option<GuitarTechnique>,
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct SetExpressionTextCmd {
511 pub measure_index: usize,
512 pub text: Option<String>,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct SetCueCmd {
519 pub part_index: usize,
520 pub staff_index: usize,
521 pub measure_index: usize,
522 pub voice: usize,
523 pub note_index: usize,
524 pub is_cue: bool,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct SetNoteHeadCmd {
530 pub part_index: usize,
531 pub staff_index: usize,
532 pub measure_index: usize,
533 pub voice: usize,
534 pub note_index: usize,
535 pub note_head: NoteHead,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
540pub struct RespellScoreCmd {
541 pub prefer_flat: bool,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct RespellScoreToKeyCmd {}
547
548struct UndoEntry {
549 command: Command,
550 snapshot: Score,
551}
552
553pub struct CommandStack {
554 history: Vec<UndoEntry>,
555 future: Vec<(Command, Score)>,
556 max_depth: usize,
557}
558
559impl CommandStack {
560 pub fn new(max_depth: usize) -> Self {
561 Self {
562 history: Vec::new(),
563 future: Vec::new(),
564 max_depth,
565 }
566 }
567
568 pub fn can_undo(&self) -> bool {
569 !self.history.is_empty()
570 }
571
572 pub fn can_redo(&self) -> bool {
573 !self.future.is_empty()
574 }
575
576 pub fn execute(&mut self, cmd: Command, score: &mut Score) -> Result<(), Error> {
577 let snapshot = score.clone();
578 apply_command(&cmd, score)?;
579 self.history.push(UndoEntry {
580 command: cmd,
581 snapshot,
582 });
583 self.future.clear();
584 if self.history.len() > self.max_depth {
585 self.history.remove(0);
586 }
587 Ok(())
588 }
589
590 pub fn undo(&mut self, score: &mut Score) -> Result<ChangeHint, Error> {
591 let entry = self.history.pop().ok_or(Error::NothingToUndo)?;
592 let hint = command_hint(&entry.command);
593 let post_snapshot = score.clone();
594 *score = entry.snapshot;
595 self.future.push((entry.command, post_snapshot));
596 if self.future.len() > self.max_depth {
597 self.future.remove(0);
598 }
599 Ok(hint)
600 }
601
602 pub fn redo(&mut self, score: &mut Score) -> Result<ChangeHint, Error> {
603 let (cmd, post) = self.future.pop().ok_or(Error::NothingToRedo)?;
604 let hint = command_hint(&cmd);
605 let snapshot = score.clone();
606 *score = post;
607 self.history.push(UndoEntry {
608 command: cmd,
609 snapshot,
610 });
611 Ok(hint)
612 }
613
614 pub fn history_commands(&self) -> Vec<Command> {
617 self.history.iter().map(|e| e.command.clone()).collect()
618 }
619
620 pub fn undo_label(&self) -> Option<String> {
622 self.history.last().map(|e| command_label(&e.command))
623 }
624
625 pub fn redo_label(&self) -> Option<String> {
627 self.future.last().map(|(cmd, _)| command_label(cmd))
628 }
629
630 pub fn undo_key(&self) -> Option<String> {
632 self.history.last().map(|e| command_key(&e.command))
633 }
634
635 pub fn redo_key(&self) -> Option<String> {
637 self.future.last().map(|(cmd, _)| command_key(cmd))
638 }
639
640 pub fn batch_execute(&mut self, cmds: Vec<Command>, score: &mut Score) -> Result<(), Error> {
642 if cmds.is_empty() {
643 return Ok(());
644 }
645 let snapshot = score.clone();
646 for cmd in &cmds {
647 if let Err(e) = apply_command(cmd, score) {
648 *score = snapshot;
649 return Err(e);
650 }
651 }
652 self.history.push(UndoEntry {
653 command: Command::Batch(BatchCmd {
654 commands: cmds,
655 label: None,
656 }),
657 snapshot,
658 });
659 self.future.clear();
660 if self.history.len() > self.max_depth {
661 self.history.remove(0);
662 }
663 Ok(())
664 }
665
666 pub fn batch_execute_labeled(
670 &mut self,
671 cmds: Vec<Command>,
672 label: String,
673 score: &mut Score,
674 ) -> Result<(), Error> {
675 if cmds.is_empty() {
676 return Ok(());
677 }
678 let snapshot = score.clone();
679 for cmd in &cmds {
680 if let Err(e) = apply_command(cmd, score) {
681 *score = snapshot;
682 return Err(e);
683 }
684 }
685 self.history.push(UndoEntry {
686 command: Command::Batch(BatchCmd {
687 commands: cmds,
688 label: Some(label),
689 }),
690 snapshot,
691 });
692 self.future.clear();
693 if self.history.len() > self.max_depth {
694 self.history.remove(0);
695 }
696 Ok(())
697 }
698}
699
700pub fn command_hint(cmd: &Command) -> ChangeHint {
703 use ChangeScope::*;
704 macro_rules! hint {
705 ($scope:expr, $layout:expr, $playback:expr) => {
706 ChangeHint {
707 scope: $scope,
708 layout_dirty: $layout,
709 playback_dirty: $playback,
710 }
711 };
712 }
713 macro_rules! meas {
714 ($c:expr) => {
715 Measures {
716 part: $c.part_index,
717 staff: $c.staff_index,
718 start: $c.measure_index,
719 end: $c.measure_index + 1,
720 }
721 };
722 }
723 match cmd {
724 Command::NewScore(_)
726 | Command::AddPart(_)
727 | Command::DeletePart(_)
728 | Command::AddMeasure(_)
729 | Command::DeleteMeasure(_) => hint!(Global, true, true),
730
731 Command::SetTempo(_) => hint!(Global, false, true),
732
733 Command::SetMetadata(_) => hint!(Global, false, false),
734
735 Command::SetKeySignature(_) => hint!(Global, true, false),
736
737 Command::SetTimeSignature(_) => hint!(Global, true, true),
738
739 Command::SetBarline(_)
740 | Command::SetVolta(_)
741 | Command::SetRehearsalMark(_)
742 | Command::SetNavigationMark(_)
743 | Command::SetExpressionText(_) => hint!(Global, false, false),
744
745 Command::SetMultiRest(_) => hint!(Global, true, false),
746
747 Command::SetTempoAtMeasure(_) => hint!(Global, false, true),
748
749 Command::SetPartName(c) => hint!(Part(c.part_index), false, false),
751
752 Command::SetMidiInstrument(c) => hint!(Part(c.part_index), false, true),
753
754 Command::SetTranspose(c) => hint!(Part(c.part_index), false, true),
755
756 Command::SetClef(c) => hint!(Part(c.part_index), true, false),
757
758 Command::AddNote(c) => hint!(meas!(c), false, true),
760 Command::AddPitch(c) => hint!(meas!(c), false, true),
761 Command::DeleteNote(c) => hint!(meas!(c), false, true),
762 Command::PasteVoice(c) => hint!(meas!(c), false, true),
763 Command::PasteRange(c) => hint!(
764 Measures {
765 part: c.part_index,
766 staff: c.staff_index,
767 start: c.target_measure,
768 end: c.target_measure + c.measures.len()
769 },
770 false,
771 true
772 ),
773 Command::AddHairpin(c) => hint!(meas!(c), false, true),
774 Command::ToggleTie(c) => hint!(meas!(c), false, true),
775 Command::SetDynamic(c) => hint!(meas!(c), false, true),
776 Command::ToggleArticulation(c) => hint!(meas!(c), false, true),
777 Command::SetGrace(c) => hint!(meas!(c), false, true),
778 Command::SetOttava(c) => hint!(meas!(c), false, true),
779 Command::SetLyric(c) => hint!(meas!(c), false, true),
780 Command::AddPedal(c) => hint!(meas!(c), false, true),
781 Command::SetChordSymbol(c) => hint!(meas!(c), false, true),
782
783 Command::SetSystemBreak(_) | Command::SetPageBreak(_) => hint!(Global, true, false),
784
785 Command::ToggleSlur(_) | Command::ToggleTrillLine(_) => hint!(Global, true, false),
786
787 Command::SetPartGroup(_) => hint!(Global, false, false),
788
789 Command::AddStaff(_) | Command::DeleteStaff(_) => hint!(Global, true, true),
790
791 Command::SetTuplet(c) => hint!(meas!(c), false, true),
792
793 Command::RespellScore(_) | Command::RespellScoreToKey(_) => hint!(Global, true, true),
794
795 Command::SetStem(c) => hint!(meas!(c), false, false),
796
797 Command::SetArpeggio(c) => hint!(meas!(c), false, false),
798
799 Command::SetTechniqueText(c) => hint!(meas!(c), false, false),
800 Command::SetFingering(c) => hint!(meas!(c), false, false),
801 Command::SetStringNumber(c) => hint!(meas!(c), false, false),
802 Command::SetGuitarTechnique(c) => hint!(meas!(c), false, false),
803 Command::SetNoteHead(c) => hint!(meas!(c), false, false),
804 Command::SetCue(c) => hint!(meas!(c), false, true),
805
806 Command::Batch(c) => {
807 let Some(first) = c.commands.first() else {
808 return hint!(Global, false, false);
809 };
810 let mut merged = command_hint(first);
811 for cmd in c.commands.iter().skip(1) {
812 merged = merged.merge(command_hint(cmd));
813 }
814 merged
815 }
816 }
817}
818
819pub fn command_label(cmd: &Command) -> String {
821 match cmd {
822 Command::AddNote(_) => "Add Note".to_string(),
823 Command::AddPitch(_) => "Add Pitch".to_string(),
824 Command::DeleteNote(_) => "Delete Note".to_string(),
825 Command::AddMeasure(_) => "Add Measure".to_string(),
826 Command::DeleteMeasure(_) => "Delete Measure".to_string(),
827 Command::SetTempo(_) => "Set Tempo".to_string(),
828 Command::NewScore(_) => "New Score".to_string(),
829 Command::AddHairpin(_) => "Add Hairpin".to_string(),
830 Command::ToggleTie(_) => "Toggle Tie".to_string(),
831 Command::SetDynamic(_) => "Set Dynamic".to_string(),
832 Command::ToggleArticulation(_) => "Toggle Articulation".to_string(),
833 Command::SetKeySignature(_) => "Set Key Signature".to_string(),
834 Command::SetTimeSignature(_) => "Set Time Signature".to_string(),
835 Command::SetBarline(_) => "Set Barline".to_string(),
836 Command::AddPart(_) => "Add Part".to_string(),
837 Command::DeletePart(_) => "Delete Part".to_string(),
838 Command::SetMetadata(_) => "Set Metadata".to_string(),
839 Command::SetRehearsalMark(_) => "Set Rehearsal Mark".to_string(),
840 Command::SetNavigationMark(_) => "Set Navigation Mark".to_string(),
841 Command::SetChordSymbol(_) => "Set Chord Symbol".to_string(),
842 Command::SetGrace(_) => "Set Grace Note".to_string(),
843 Command::SetOttava(_) => "Set Ottava".to_string(),
844 Command::SetLyric(_) => "Set Lyric".to_string(),
845 Command::SetMultiRest(_) => "Set Multi-Rest".to_string(),
846 Command::AddPedal(_) => "Add Pedal".to_string(),
847 Command::SetVolta(_) => "Set Volta".to_string(),
848 Command::SetClef(_) => "Set Clef".to_string(),
849 Command::SetPartName(_) => "Set Part Name".to_string(),
850 Command::SetMidiInstrument(_) => "Set MIDI Instrument".to_string(),
851 Command::SetTranspose(_) => "Set Transpose".to_string(),
852 Command::SetTempoAtMeasure(_) => "Set Tempo".to_string(),
853 Command::PasteVoice(_) => "Paste Voice".to_string(),
854 Command::PasteRange(_) => "Paste Range".to_string(),
855 Command::SetSystemBreak(_) => "Set System Break".to_string(),
856 Command::SetPageBreak(_) => "Set Page Break".to_string(),
857 Command::ToggleSlur(_) => "Toggle Slur".to_string(),
858 Command::AddStaff(_) => "Add Staff".to_string(),
859 Command::DeleteStaff(_) => "Delete Staff".to_string(),
860 Command::SetTuplet(c) => if c.tuplet.is_some() {
861 "Set Tuplet"
862 } else {
863 "Clear Tuplet"
864 }
865 .to_string(),
866 Command::RespellScore(c) => if c.prefer_flat {
867 "Respell Score (flat)"
868 } else {
869 "Respell Score (sharp)"
870 }
871 .to_string(),
872 Command::RespellScoreToKey(_) => "Respell Score to Key".to_string(),
873 Command::SetStem(_) => "Set Stem".to_string(),
874 Command::SetArpeggio(_) => "Set Arpeggio".to_string(),
875 Command::SetTechniqueText(_) => "Set Technique Text".to_string(),
876 Command::SetFingering(_) => "Set Fingering".to_string(),
877 Command::SetStringNumber(_) => "Set String Number".to_string(),
878 Command::SetGuitarTechnique(_) => "Set Guitar Technique".to_string(),
879 Command::SetNoteHead(_) => "Set Note Head".to_string(),
880 Command::SetCue(c) => if c.is_cue {
881 "Set Cue Note"
882 } else {
883 "Clear Cue Note"
884 }
885 .to_string(),
886 Command::SetExpressionText(_) => "Set Expression Text".to_string(),
887 Command::ToggleTrillLine(_) => "Toggle Trill Line".to_string(),
888 Command::SetPartGroup(_) => "Set Part Group".to_string(),
889 Command::Batch(c) => c.label.clone().unwrap_or_else(|| {
890 c.commands
891 .first()
892 .map(command_label)
893 .unwrap_or_else(|| "Batch".to_string())
894 }),
895 }
896}
897
898pub fn command_key(cmd: &Command) -> String {
902 match cmd {
903 Command::AddNote(_) => "AddNote".to_string(),
904 Command::AddPitch(_) => "AddPitch".to_string(),
905 Command::DeleteNote(_) => "DeleteNote".to_string(),
906 Command::AddMeasure(_) => "AddMeasure".to_string(),
907 Command::DeleteMeasure(_) => "DeleteMeasure".to_string(),
908 Command::SetTempo(_) => "SetTempo".to_string(),
909 Command::NewScore(_) => "NewScore".to_string(),
910 Command::AddHairpin(_) => "AddHairpin".to_string(),
911 Command::ToggleTie(_) => "ToggleTie".to_string(),
912 Command::SetDynamic(_) => "SetDynamic".to_string(),
913 Command::ToggleArticulation(_) => "ToggleArticulation".to_string(),
914 Command::SetKeySignature(_) => "SetKeySignature".to_string(),
915 Command::SetTimeSignature(_) => "SetTimeSignature".to_string(),
916 Command::SetBarline(_) => "SetBarline".to_string(),
917 Command::AddPart(_) => "AddPart".to_string(),
918 Command::DeletePart(_) => "DeletePart".to_string(),
919 Command::SetMetadata(_) => "SetMetadata".to_string(),
920 Command::SetRehearsalMark(_) => "SetRehearsalMark".to_string(),
921 Command::SetNavigationMark(_) => "SetNavigationMark".to_string(),
922 Command::SetChordSymbol(_) => "SetChordSymbol".to_string(),
923 Command::SetGrace(_) => "SetGrace".to_string(),
924 Command::SetOttava(_) => "SetOttava".to_string(),
925 Command::SetLyric(_) => "SetLyric".to_string(),
926 Command::SetMultiRest(_) => "SetMultiRest".to_string(),
927 Command::AddPedal(_) => "AddPedal".to_string(),
928 Command::SetVolta(_) => "SetVolta".to_string(),
929 Command::SetClef(_) => "SetClef".to_string(),
930 Command::SetPartName(_) => "SetPartName".to_string(),
931 Command::SetMidiInstrument(_) => "SetMidiInstrument".to_string(),
932 Command::SetTranspose(_) => "SetTranspose".to_string(),
933 Command::SetTempoAtMeasure(_) => "SetTempoAtMeasure".to_string(),
934 Command::PasteVoice(_) => "PasteVoice".to_string(),
935 Command::PasteRange(_) => "PasteRange".to_string(),
936 Command::SetSystemBreak(_) => "SetSystemBreak".to_string(),
937 Command::SetPageBreak(_) => "SetPageBreak".to_string(),
938 Command::ToggleSlur(_) => "ToggleSlur".to_string(),
939 Command::AddStaff(_) => "AddStaff".to_string(),
940 Command::DeleteStaff(_) => "DeleteStaff".to_string(),
941 Command::SetTuplet(_) => "SetTuplet".to_string(),
942 Command::RespellScore(_) => "RespellScore".to_string(),
943 Command::RespellScoreToKey(_) => "RespellScoreToKey".to_string(),
944 Command::SetStem(_) => "SetStem".to_string(),
945 Command::SetArpeggio(_) => "SetArpeggio".to_string(),
946 Command::SetTechniqueText(_) => "SetTechniqueText".to_string(),
947 Command::SetFingering(_) => "SetFingering".to_string(),
948 Command::SetStringNumber(_) => "SetStringNumber".to_string(),
949 Command::SetGuitarTechnique(_) => "SetGuitarTechnique".to_string(),
950 Command::SetNoteHead(_) => "SetNoteHead".to_string(),
951 Command::SetCue(_) => "SetCue".to_string(),
952 Command::SetExpressionText(_) => "SetExpressionText".to_string(),
953 Command::ToggleTrillLine(_) => "ToggleTrillLine".to_string(),
954 Command::SetPartGroup(_) => "SetPartGroup".to_string(),
955 Command::Batch(c) => c.label.clone().unwrap_or_else(|| "Batch".to_string()),
956 }
957}
958
959pub fn apply_command(cmd: &Command, score: &mut Score) -> Result<(), Error> {
960 match cmd {
961 Command::AddNote(c) => apply_add_note(c, score),
962 Command::AddPitch(c) => apply_add_pitch(c, score),
963 Command::DeleteNote(c) => apply_delete_note(c, score),
964 Command::AddMeasure(c) => apply_add_measure(c, score),
965 Command::DeleteMeasure(c) => apply_delete_measure(c, score),
966 Command::SetTempo(c) => {
967 score.settings.tempo_bpm = c.bpm;
968 Ok(())
969 }
970 Command::NewScore(c) => {
971 let mut s = match c.template {
972 Some(kind) => Score::template(kind),
973 None => Score::new(
974 &c.title,
975 c.tempo_bpm,
976 c.time_numerator,
977 c.time_denominator,
978 c.key_fifths,
979 c.measure_count,
980 ),
981 };
982 if c.template.is_some() {
983 s.metadata.title = c.title.clone();
984 s.metadata.composer = c.composer.clone();
985 s.settings.tempo_bpm = c.tempo_bpm;
986 s.settings.time_signature = TimeSignature {
987 numerator: c.time_numerator,
988 denominator: c.time_denominator,
989 };
990 s.settings.key_signature = KeySignature {
991 fifths: c.key_fifths,
992 mode: "major".to_string(),
993 };
994 for part in &mut s.parts {
995 for staff in &mut part.staves {
996 staff.measures.clear();
997 for i in 0..c.measure_count {
998 let mut m = Measure::empty(c.time_numerator, c.time_denominator);
999 m.number = i + 1;
1000 staff.measures.push(m);
1001 }
1002 }
1003 }
1004 }
1005 *score = s;
1006 Ok(())
1007 }
1008 Command::AddHairpin(c) => apply_add_hairpin(c, score),
1009 Command::ToggleTie(c) => apply_toggle_tie(c, score),
1010 Command::SetDynamic(c) => apply_set_dynamic(c, score),
1011 Command::ToggleArticulation(c) => apply_toggle_articulation(c, score),
1012 Command::SetKeySignature(c) => {
1013 score.settings.key_signature = KeySignature {
1014 fifths: c.fifths,
1015 mode: "major".to_string(),
1016 };
1017 Ok(())
1018 }
1019 Command::SetTimeSignature(c) => apply_set_time_signature(c, score),
1020 Command::SetBarline(c) => apply_set_barline(c, score),
1021 Command::AddPart(c) => apply_add_part(c, score),
1022 Command::DeletePart(c) => apply_delete_part(c, score),
1023 Command::SetMetadata(c) => apply_set_metadata(c, score),
1024 Command::SetRehearsalMark(c) => {
1025 for_each_measure_at(score, c.measure_index, |m| {
1026 m.rehearsal = c.text.clone();
1027 });
1028 Ok(())
1029 }
1030 Command::SetNavigationMark(c) => {
1031 for_each_measure_at(score, c.measure_index, |m| {
1032 m.navigation = c.mark.clone();
1033 });
1034 Ok(())
1035 }
1036 Command::SetChordSymbol(c) => {
1037 get_note_mut(
1038 score,
1039 c.part_index,
1040 c.staff_index,
1041 c.measure_index,
1042 c.voice,
1043 c.note_index,
1044 )?
1045 .chord_symbol = c.chord.clone();
1046 Ok(())
1047 }
1048 Command::SetGrace(c) => {
1049 let note = get_note_mut(
1050 score,
1051 c.part_index,
1052 c.staff_index,
1053 c.measure_index,
1054 c.voice,
1055 c.note_index,
1056 )?;
1057 if note.is_rest {
1058 return Err(Error::InvalidCommand(
1059 "cannot make a rest into a grace note".into(),
1060 ));
1061 }
1062 note.is_grace = c.is_grace;
1063 note.grace_slash = c.slash;
1064 Ok(())
1065 }
1066 Command::SetOttava(c) => {
1067 let note = get_note_mut(
1068 score,
1069 c.part_index,
1070 c.staff_index,
1071 c.measure_index,
1072 c.voice,
1073 c.note_index,
1074 )?;
1075 note.ottava_start = c.ottava_start;
1076 note.ottava_end = c.ottava_end;
1077 Ok(())
1078 }
1079 Command::SetLyric(c) => {
1080 get_note_mut(
1081 score,
1082 c.part_index,
1083 c.staff_index,
1084 c.measure_index,
1085 c.voice,
1086 c.note_index,
1087 )?
1088 .lyric = c.lyric.clone();
1089 Ok(())
1090 }
1091 Command::SetMultiRest(c) => {
1092 for_each_measure_at(score, c.measure_index, |m| {
1093 m.multi_rest_count = c.count;
1094 });
1095 Ok(())
1096 }
1097 Command::AddPedal(c) => apply_add_pedal(c, score),
1098 Command::SetVolta(c) => {
1099 for_each_measure_at(score, c.measure_index, |m| {
1100 m.volta = c.volta.clone();
1101 });
1102 Ok(())
1103 }
1104 Command::SetClef(c) => apply_set_clef(c, score),
1105 Command::SetPartName(c) => apply_set_part_name(c, score),
1106 Command::SetMidiInstrument(c) => apply_set_midi_instrument(c, score),
1107 Command::SetTranspose(c) => apply_set_transpose(c, score),
1108 Command::SetTempoAtMeasure(c) => {
1109 for_each_measure_at(score, c.measure_index, |m| {
1110 m.tempo = c.bpm;
1111 });
1112 Ok(())
1113 }
1114 Command::PasteVoice(c) => apply_paste_voice(c, score),
1115 Command::PasteRange(c) => apply_paste_range(c, score),
1116 Command::SetSystemBreak(c) => {
1117 for_each_measure_at(score, c.measure_index, |m| {
1118 m.system_break = c.value;
1119 });
1120 Ok(())
1121 }
1122 Command::SetPageBreak(c) => {
1123 for_each_measure_at(score, c.measure_index, |m| {
1124 m.page_break = c.value;
1125 });
1126 Ok(())
1127 }
1128 Command::ToggleSlur(c) => apply_toggle_slur(c, score),
1129 Command::AddStaff(c) => apply_add_staff(c, score),
1130 Command::DeleteStaff(c) => apply_delete_staff(c, score),
1131 Command::SetTuplet(c) => {
1132 get_note_mut(
1133 score,
1134 c.part_index,
1135 c.staff_index,
1136 c.measure_index,
1137 c.voice_index,
1138 c.note_index,
1139 )?
1140 .tuplet = c.tuplet.clone();
1141 Ok(())
1142 }
1143 Command::RespellScore(c) => {
1144 respell_score(score, c.prefer_flat);
1145 Ok(())
1146 }
1147 Command::RespellScoreToKey(_) => {
1148 respell_score_to_key(score);
1149 Ok(())
1150 }
1151 Command::SetStem(c) => {
1152 get_note_mut(
1153 score,
1154 c.part_index,
1155 c.staff_index,
1156 c.measure_index,
1157 c.voice_index,
1158 c.note_index,
1159 )?
1160 .stem_up = c.stem_up;
1161 Ok(())
1162 }
1163 Command::SetArpeggio(c) => {
1164 get_note_mut(
1165 score,
1166 c.part_index,
1167 c.staff_index,
1168 c.measure_index,
1169 c.voice_index,
1170 c.note_index,
1171 )?
1172 .arpeggiate = c.direction;
1173 Ok(())
1174 }
1175 Command::SetTechniqueText(c) => {
1176 get_note_mut(
1177 score,
1178 c.part_index,
1179 c.staff_index,
1180 c.measure_index,
1181 c.voice,
1182 c.note_index,
1183 )?
1184 .technique_text = c.text.clone();
1185 Ok(())
1186 }
1187 Command::SetFingering(c) => {
1188 get_note_mut(
1189 score,
1190 c.part_index,
1191 c.staff_index,
1192 c.measure_index,
1193 c.voice,
1194 c.note_index,
1195 )?
1196 .fingering = c.fingering;
1197 Ok(())
1198 }
1199 Command::SetStringNumber(c) => {
1200 get_note_mut(
1201 score,
1202 c.part_index,
1203 c.staff_index,
1204 c.measure_index,
1205 c.voice,
1206 c.note_index,
1207 )?
1208 .string_number = c.string_number;
1209 Ok(())
1210 }
1211 Command::SetGuitarTechnique(c) => {
1212 get_note_mut(
1213 score,
1214 c.part_index,
1215 c.staff_index,
1216 c.measure_index,
1217 c.voice,
1218 c.note_index,
1219 )?
1220 .guitar_technique = c.technique.clone();
1221 Ok(())
1222 }
1223 Command::SetNoteHead(c) => {
1224 get_note_mut(
1225 score,
1226 c.part_index,
1227 c.staff_index,
1228 c.measure_index,
1229 c.voice,
1230 c.note_index,
1231 )?
1232 .note_head = c.note_head.clone();
1233 Ok(())
1234 }
1235 Command::SetCue(c) => {
1236 get_note_mut(
1237 score,
1238 c.part_index,
1239 c.staff_index,
1240 c.measure_index,
1241 c.voice,
1242 c.note_index,
1243 )?
1244 .is_cue = c.is_cue;
1245 Ok(())
1246 }
1247 Command::SetExpressionText(c) => {
1248 for_each_measure_at(score, c.measure_index, |m| {
1249 m.expression_text = c.text.clone();
1250 });
1251 Ok(())
1252 }
1253 Command::ToggleTrillLine(c) => apply_toggle_trill_line(c, score),
1254 Command::SetPartGroup(c) => {
1255 if let Some(group) = &c.group {
1256 score
1257 .part_groups
1258 .retain(|g| g.first_part != group.first_part || g.last_part != group.last_part);
1259 score.part_groups.push(group.clone());
1260 } else {
1261 score.part_groups.clear();
1263 }
1264 Ok(())
1265 }
1266 Command::Batch(c) => {
1267 for cmd in &c.commands {
1268 apply_command(cmd, score)?;
1269 }
1270 Ok(())
1271 }
1272 }
1273}
1274
1275fn get_note_mut(
1278 score: &mut Score,
1279 part_index: usize,
1280 staff_index: usize,
1281 measure_index: usize,
1282 voice: usize,
1283 note_index: usize,
1284) -> Result<&mut Note, Error> {
1285 score
1286 .parts
1287 .get_mut(part_index)
1288 .ok_or(Error::PartNotFound(part_index))?
1289 .staves
1290 .get_mut(staff_index)
1291 .ok_or(Error::StaffNotFound(staff_index))?
1292 .measures
1293 .get_mut(measure_index)
1294 .ok_or(Error::MeasureNotFound(measure_index))?
1295 .voices
1296 .get_mut(voice)
1297 .ok_or(Error::VoiceOutOfRange(voice))?
1298 .get_mut(note_index)
1299 .ok_or(Error::NoteNotFound(note_index))
1300}
1301
1302fn for_each_measure_at(score: &mut Score, index: usize, mut f: impl FnMut(&mut Measure)) {
1303 for part in &mut score.parts {
1304 for staff in &mut part.staves {
1305 if let Some(m) = staff.measures.get_mut(index) {
1306 f(m);
1307 }
1308 }
1309 }
1310}
1311
1312fn apply_add_note(cmd: &AddNoteCmd, score: &mut Score) -> Result<(), Error> {
1313 let ts_beats = score.settings.time_signature.total_beats();
1314 let voice = score
1315 .parts
1316 .get_mut(cmd.part_index)
1317 .ok_or(Error::PartNotFound(cmd.part_index))?
1318 .staves
1319 .get_mut(cmd.staff_index)
1320 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1321 .measures
1322 .get_mut(cmd.measure_index)
1323 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
1324 .voices
1325 .get_mut(cmd.voice)
1326 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1327
1328 let note = if cmd.is_rest {
1329 let mut n = Note::rest(cmd.duration.clone());
1330 n.dot_count = cmd.dot_count;
1331 n.tuplet = cmd.tuplet.clone();
1332 n
1333 } else {
1334 let pitch = cmd
1335 .pitch
1336 .clone()
1337 .ok_or_else(|| Error::InvalidCommand("pitch required for non-rest note".into()))?;
1338 let mut n = Note::new(pitch, cmd.duration.clone());
1339 n.dot_count = cmd.dot_count;
1340 n.tuplet = cmd.tuplet.clone();
1341 n
1342 };
1343
1344 let pos = cmd.position.min(voice.len());
1345 voice.insert(pos, note);
1346 trim_voice_to_measure(voice, ts_beats);
1347 Ok(())
1348}
1349
1350fn apply_add_pitch(cmd: &AddPitchCmd, score: &mut Score) -> Result<(), Error> {
1351 let voice = score
1352 .parts
1353 .get_mut(cmd.part_index)
1354 .ok_or(Error::PartNotFound(cmd.part_index))?
1355 .staves
1356 .get_mut(cmd.staff_index)
1357 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1358 .measures
1359 .get_mut(cmd.measure_index)
1360 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
1361 .voices
1362 .get_mut(cmd.voice)
1363 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1364 let note = voice
1365 .get_mut(cmd.note_index)
1366 .ok_or(Error::NoteNotFound(cmd.note_index))?;
1367 if note.is_rest {
1368 return Err(Error::InvalidCommand("cannot add pitch to a rest".into()));
1369 }
1370 if !note
1371 .pitches
1372 .iter()
1373 .any(|p| p.step == cmd.pitch.step && p.octave == cmd.pitch.octave)
1374 {
1375 note.pitches.push(cmd.pitch.clone());
1376 }
1377 Ok(())
1378}
1379
1380fn apply_delete_note(cmd: &DeleteNoteCmd, score: &mut Score) -> Result<(), Error> {
1381 let ts_beats = score.settings.time_signature.total_beats();
1382 let voice = score
1383 .parts
1384 .get_mut(cmd.part_index)
1385 .ok_or(Error::PartNotFound(cmd.part_index))?
1386 .staves
1387 .get_mut(cmd.staff_index)
1388 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1389 .measures
1390 .get_mut(cmd.measure_index)
1391 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
1392 .voices
1393 .get_mut(cmd.voice)
1394 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1395 voice.retain(|n| n.id != cmd.note_id);
1396 pad_voice_to_measure(voice, ts_beats);
1397 Ok(())
1398}
1399
1400fn apply_add_measure(cmd: &AddMeasureCmd, score: &mut Score) -> Result<(), Error> {
1401 let ts = score.settings.time_signature.clone();
1402 for part in &mut score.parts {
1403 for staff in &mut part.staves {
1404 let insert_at = (cmd.after_index + 1).min(staff.measures.len());
1405 let mut m = Measure::empty(ts.numerator, ts.denominator);
1406 m.number = insert_at as u32 + 1;
1407 staff.measures.insert(insert_at, m);
1408 for (i, measure) in staff.measures.iter_mut().enumerate() {
1409 measure.number = i as u32 + 1;
1410 }
1411 }
1412 }
1413 Ok(())
1414}
1415
1416fn apply_delete_measure(cmd: &DeleteMeasureCmd, score: &mut Score) -> Result<(), Error> {
1417 for part in &mut score.parts {
1418 for staff in &mut part.staves {
1419 if cmd.measure_index < staff.measures.len() {
1420 staff.measures.remove(cmd.measure_index);
1421 for (i, m) in staff.measures.iter_mut().enumerate() {
1422 m.number = i as u32 + 1;
1423 }
1424 }
1425 }
1426 }
1427 Ok(())
1428}
1429
1430fn apply_add_hairpin(cmd: &AddHairpinCmd, score: &mut Score) -> Result<(), Error> {
1431 let voice = score
1432 .parts
1433 .get_mut(cmd.part_index)
1434 .ok_or(Error::PartNotFound(cmd.part_index))?
1435 .staves
1436 .get_mut(cmd.staff_index)
1437 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1438 .measures
1439 .get_mut(cmd.measure_index)
1440 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
1441 .voices
1442 .get_mut(cmd.voice)
1443 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1444 if cmd.start_note_idx >= voice.len() {
1445 return Err(Error::NoteNotFound(cmd.start_note_idx));
1446 }
1447 if cmd.end_note_idx >= voice.len() {
1448 return Err(Error::NoteNotFound(cmd.end_note_idx));
1449 }
1450 if cmd.start_note_idx >= cmd.end_note_idx {
1451 return Err(Error::InvalidCommand(
1452 "start_note_idx must be less than end_note_idx".into(),
1453 ));
1454 }
1455 for note in voice
1456 .iter_mut()
1457 .take(cmd.end_note_idx + 1)
1458 .skip(cmd.start_note_idx)
1459 {
1460 note.hairpin_start = None;
1461 note.hairpin_end = false;
1462 }
1463 voice[cmd.start_note_idx].hairpin_start = Some(cmd.kind);
1464 voice[cmd.end_note_idx].hairpin_end = true;
1465 Ok(())
1466}
1467
1468fn apply_add_pedal(cmd: &AddPedalCmd, score: &mut Score) -> Result<(), Error> {
1469 let voice = score
1470 .parts
1471 .get_mut(cmd.part_index)
1472 .ok_or(Error::PartNotFound(cmd.part_index))?
1473 .staves
1474 .get_mut(cmd.staff_index)
1475 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1476 .measures
1477 .get_mut(cmd.measure_index)
1478 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
1479 .voices
1480 .get_mut(cmd.voice)
1481 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1482 if cmd.start_note_idx >= voice.len() {
1483 return Err(Error::NoteNotFound(cmd.start_note_idx));
1484 }
1485 if cmd.end_note_idx >= voice.len() {
1486 return Err(Error::NoteNotFound(cmd.end_note_idx));
1487 }
1488 if cmd.start_note_idx >= cmd.end_note_idx {
1489 return Err(Error::InvalidCommand(
1490 "start_note_idx must be less than end_note_idx".into(),
1491 ));
1492 }
1493 for note in voice
1494 .iter_mut()
1495 .take(cmd.end_note_idx + 1)
1496 .skip(cmd.start_note_idx)
1497 {
1498 note.pedal_start = false;
1499 note.pedal_end = false;
1500 }
1501 voice[cmd.start_note_idx].pedal_start = true;
1502 voice[cmd.end_note_idx].pedal_end = true;
1503 Ok(())
1504}
1505
1506fn apply_toggle_tie(cmd: &ToggleTieCmd, score: &mut Score) -> Result<(), Error> {
1507 let current_tie_start = {
1508 let v = score
1509 .parts
1510 .get(cmd.part_index)
1511 .ok_or(Error::PartNotFound(cmd.part_index))?
1512 .staves
1513 .get(cmd.staff_index)
1514 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1515 .measures
1516 .get(cmd.measure_index)
1517 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
1518 .voices
1519 .get(cmd.voice)
1520 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1521 v.get(cmd.note_index)
1522 .ok_or(Error::NoteNotFound(cmd.note_index))?
1523 .tie_start
1524 };
1525 let voice_len = score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index]
1526 .voices[cmd.voice]
1527 .len();
1528 let total_measures = score.parts[cmd.part_index].staves[cmd.staff_index]
1529 .measures
1530 .len();
1531
1532 let new_tie = !current_tie_start;
1533 score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index].voices
1534 [cmd.voice][cmd.note_index]
1535 .tie_start = new_tie;
1536
1537 if cmd.note_index + 1 < voice_len {
1538 score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index].voices
1539 [cmd.voice][cmd.note_index + 1]
1540 .tie_end = new_tie;
1541 } else {
1542 let next_mi = cmd.measure_index + 1;
1543 if next_mi < total_measures {
1544 let next_voice = &mut score.parts[cmd.part_index].staves[cmd.staff_index].measures
1545 [next_mi]
1546 .voices[cmd.voice];
1547 if let Some(n) = next_voice.get_mut(0) {
1548 n.tie_end = new_tie;
1549 }
1550 }
1551 }
1552 Ok(())
1553}
1554
1555fn apply_set_dynamic(cmd: &SetDynamicCmd, score: &mut Score) -> Result<(), Error> {
1556 get_note_mut(
1557 score,
1558 cmd.part_index,
1559 cmd.staff_index,
1560 cmd.measure_index,
1561 cmd.voice,
1562 cmd.note_index,
1563 )?
1564 .dynamic = cmd.dynamic.clone();
1565 Ok(())
1566}
1567
1568fn apply_toggle_articulation(cmd: &ToggleArticulationCmd, score: &mut Score) -> Result<(), Error> {
1569 let note = get_note_mut(
1570 score,
1571 cmd.part_index,
1572 cmd.staff_index,
1573 cmd.measure_index,
1574 cmd.voice,
1575 cmd.note_index,
1576 )?;
1577 if let Some(pos) = note
1578 .articulations
1579 .iter()
1580 .position(|a| a == &cmd.articulation)
1581 {
1582 note.articulations.remove(pos);
1583 } else {
1584 note.articulations.push(cmd.articulation.clone());
1585 }
1586 Ok(())
1587}
1588
1589fn apply_set_time_signature(cmd: &SetTimeSignatureCmd, score: &mut Score) -> Result<(), Error> {
1590 if cmd.numerator == 0 || cmd.denominator == 0 {
1591 return Err(Error::InvalidCommand(
1592 "time signature numerator and denominator must be > 0".into(),
1593 ));
1594 }
1595 if ![1u8, 2, 4, 8, 16, 32].contains(&cmd.denominator) {
1596 return Err(Error::InvalidCommand(format!(
1597 "invalid time signature denominator: {}",
1598 cmd.denominator
1599 )));
1600 }
1601 score.settings.time_signature = TimeSignature {
1602 numerator: cmd.numerator,
1603 denominator: cmd.denominator,
1604 };
1605 let max_beats = score.settings.time_signature.total_beats();
1606 for part in &mut score.parts {
1607 for staff in &mut part.staves {
1608 for measure in &mut staff.measures {
1609 for voice in &mut measure.voices {
1610 trim_voice_to_measure(voice, max_beats);
1611 pad_voice_to_measure(voice, max_beats);
1612 }
1613 }
1614 }
1615 }
1616 Ok(())
1617}
1618
1619fn apply_set_barline(cmd: &SetBarlineCmd, score: &mut Score) -> Result<(), Error> {
1620 for part in &mut score.parts {
1621 for staff in &mut part.staves {
1622 let measure = staff
1623 .measures
1624 .get_mut(cmd.measure_index)
1625 .ok_or(Error::MeasureNotFound(cmd.measure_index))?;
1626 match cmd.side.as_str() {
1627 "left" => measure.barline_left = cmd.barline.clone(),
1628 "right" => measure.barline_right = cmd.barline.clone(),
1629 _ => {
1630 return Err(Error::InvalidCommand(format!(
1631 "invalid barline side: '{}'",
1632 cmd.side
1633 )));
1634 }
1635 }
1636 }
1637 }
1638 Ok(())
1639}
1640
1641fn apply_add_part(cmd: &AddPartCmd, score: &mut Score) -> Result<(), Error> {
1642 if cmd.clefs.is_empty() {
1643 return Err(Error::InvalidCommand(
1644 "AddPart requires at least one clef".into(),
1645 ));
1646 }
1647 let measure_count = score.measure_count();
1648 let ts = score.settings.time_signature.clone();
1649 let mut part = Part::new(&cmd.name, &cmd.short_name);
1650 part.midi_channel = cmd.midi_channel.min(15);
1651 part.midi_program = cmd.midi_program;
1652 for clef_str in &cmd.clefs {
1653 let clef = match clef_str.as_str() {
1654 "Bass" => Clef::Bass,
1655 "Alto" => Clef::Alto,
1656 "Tenor" => Clef::Tenor,
1657 "Percussion" => Clef::Percussion,
1658 _ => Clef::Treble,
1659 };
1660 let mut staff = Staff::new(clef);
1661 for i in 0..measure_count {
1662 let mut m = Measure::empty(ts.numerator, ts.denominator);
1663 m.number = i as u32 + 1;
1664 staff.measures.push(m);
1665 }
1666 part.staves.push(staff);
1667 }
1668 score.parts.push(part);
1669 Ok(())
1670}
1671
1672fn apply_set_clef(cmd: &SetClefCmd, score: &mut Score) -> Result<(), Error> {
1673 score
1674 .parts
1675 .get_mut(cmd.part_index)
1676 .ok_or(Error::PartNotFound(cmd.part_index))?
1677 .staves
1678 .get_mut(cmd.staff_index)
1679 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1680 .clef = cmd.clef.clone();
1681 Ok(())
1682}
1683
1684fn apply_set_part_name(cmd: &SetPartNameCmd, score: &mut Score) -> Result<(), Error> {
1685 let part = score
1686 .parts
1687 .get_mut(cmd.part_index)
1688 .ok_or(Error::PartNotFound(cmd.part_index))?;
1689 part.name = cmd.name.clone();
1690 part.short_name = cmd.short_name.clone();
1691 Ok(())
1692}
1693
1694fn apply_delete_part(cmd: &DeletePartCmd, score: &mut Score) -> Result<(), Error> {
1695 if cmd.part_index >= score.parts.len() {
1696 return Err(Error::PartNotFound(cmd.part_index));
1697 }
1698 score.parts.remove(cmd.part_index);
1699 Ok(())
1700}
1701
1702fn apply_set_metadata(cmd: &SetMetadataCmd, score: &mut Score) -> Result<(), Error> {
1703 if let Some(v) = &cmd.title {
1704 score.metadata.title = v.clone();
1705 }
1706 if let Some(v) = &cmd.composer {
1707 score.metadata.composer = v.clone();
1708 }
1709 if let Some(v) = &cmd.lyricist {
1710 score.metadata.lyricist = v.clone();
1711 }
1712 if let Some(v) = &cmd.copyright {
1713 score.metadata.copyright = v.clone();
1714 }
1715 if let Some(v) = &cmd.work_number {
1716 score.metadata.work_number = v.clone();
1717 }
1718 if let Some(v) = &cmd.movement_title {
1719 score.metadata.movement_title = v.clone();
1720 }
1721 Ok(())
1722}
1723
1724fn apply_set_midi_instrument(cmd: &SetMidiInstrumentCmd, score: &mut Score) -> Result<(), Error> {
1725 let part = score
1726 .parts
1727 .get_mut(cmd.part_index)
1728 .ok_or(Error::PartNotFound(cmd.part_index))?;
1729 part.midi_channel = cmd.midi_channel.min(15);
1730 part.midi_program = cmd.midi_program;
1731 Ok(())
1732}
1733
1734fn apply_set_transpose(cmd: &SetTransposeCmd, score: &mut Score) -> Result<(), Error> {
1735 score
1736 .parts
1737 .get_mut(cmd.part_index)
1738 .ok_or(Error::PartNotFound(cmd.part_index))?
1739 .staves
1740 .get_mut(cmd.staff_index)
1741 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1742 .transpose_semitones = cmd.semitones;
1743 Ok(())
1744}
1745
1746fn apply_paste_voice(cmd: &PasteVoiceCmd, score: &mut Score) -> Result<(), Error> {
1747 let voice = score
1748 .parts
1749 .get_mut(cmd.part_index)
1750 .ok_or(Error::PartNotFound(cmd.part_index))?
1751 .staves
1752 .get_mut(cmd.staff_index)
1753 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1754 .measures
1755 .get_mut(cmd.measure_index)
1756 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
1757 .voices
1758 .get_mut(cmd.voice_index)
1759 .ok_or(Error::VoiceOutOfRange(cmd.voice_index))?;
1760 *voice = cmd.notes.clone();
1761 Ok(())
1762}
1763
1764fn apply_paste_range(cmd: &PasteRangeCmd, score: &mut Score) -> Result<(), Error> {
1765 let part = score
1766 .parts
1767 .get_mut(cmd.part_index)
1768 .ok_or(Error::PartNotFound(cmd.part_index))?;
1769 let staff = part
1770 .staves
1771 .get_mut(cmd.staff_index)
1772 .ok_or(Error::StaffNotFound(cmd.staff_index))?;
1773 if cmd.voice_index >= 4 {
1774 return Err(Error::VoiceOutOfRange(cmd.voice_index));
1775 }
1776 for (offset, notes) in cmd.measures.iter().enumerate() {
1777 let mi = cmd.target_measure + offset;
1778 let measure = staff
1779 .measures
1780 .get_mut(mi)
1781 .ok_or(Error::MeasureNotFound(mi))?;
1782 measure.voices[cmd.voice_index] = notes.clone();
1783 }
1784 Ok(())
1785}
1786
1787fn apply_toggle_slur(cmd: &ToggleSlurCmd, score: &mut Score) -> Result<(), Error> {
1788 let new_start = !{
1789 score
1790 .parts
1791 .get(cmd.start.part)
1792 .ok_or(Error::PartNotFound(cmd.start.part))?
1793 .staves
1794 .get(cmd.start.staff)
1795 .ok_or(Error::StaffNotFound(cmd.start.staff))?
1796 .measures
1797 .get(cmd.start.measure)
1798 .ok_or(Error::MeasureNotFound(cmd.start.measure))?
1799 .voices
1800 .get(cmd.start.voice)
1801 .ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
1802 .get(cmd.start.note)
1803 .ok_or(Error::NoteNotFound(cmd.start.note))?
1804 .slur_start
1805 };
1806 let new_end = !{
1807 score
1808 .parts
1809 .get(cmd.end.part)
1810 .ok_or(Error::PartNotFound(cmd.end.part))?
1811 .staves
1812 .get(cmd.end.staff)
1813 .ok_or(Error::StaffNotFound(cmd.end.staff))?
1814 .measures
1815 .get(cmd.end.measure)
1816 .ok_or(Error::MeasureNotFound(cmd.end.measure))?
1817 .voices
1818 .get(cmd.end.voice)
1819 .ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
1820 .get(cmd.end.note)
1821 .ok_or(Error::NoteNotFound(cmd.end.note))?
1822 .slur_end
1823 };
1824 score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
1825 [cmd.start.voice][cmd.start.note]
1826 .slur_start = new_start;
1827 score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
1828 [cmd.end.voice][cmd.end.note]
1829 .slur_end = new_end;
1830 Ok(())
1831}
1832
1833fn apply_toggle_trill_line(cmd: &ToggleTrillLineCmd, score: &mut Score) -> Result<(), Error> {
1834 let new_start = !{
1835 score
1836 .parts
1837 .get(cmd.start.part)
1838 .ok_or(Error::PartNotFound(cmd.start.part))?
1839 .staves
1840 .get(cmd.start.staff)
1841 .ok_or(Error::StaffNotFound(cmd.start.staff))?
1842 .measures
1843 .get(cmd.start.measure)
1844 .ok_or(Error::MeasureNotFound(cmd.start.measure))?
1845 .voices
1846 .get(cmd.start.voice)
1847 .ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
1848 .get(cmd.start.note)
1849 .ok_or(Error::NoteNotFound(cmd.start.note))?
1850 .trill_line_start
1851 };
1852 let new_end = !{
1853 score
1854 .parts
1855 .get(cmd.end.part)
1856 .ok_or(Error::PartNotFound(cmd.end.part))?
1857 .staves
1858 .get(cmd.end.staff)
1859 .ok_or(Error::StaffNotFound(cmd.end.staff))?
1860 .measures
1861 .get(cmd.end.measure)
1862 .ok_or(Error::MeasureNotFound(cmd.end.measure))?
1863 .voices
1864 .get(cmd.end.voice)
1865 .ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
1866 .get(cmd.end.note)
1867 .ok_or(Error::NoteNotFound(cmd.end.note))?
1868 .trill_line_end
1869 };
1870 score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
1871 [cmd.start.voice][cmd.start.note]
1872 .trill_line_start = new_start;
1873 score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
1874 [cmd.end.voice][cmd.end.note]
1875 .trill_line_end = new_end;
1876 Ok(())
1877}
1878
1879fn apply_add_staff(cmd: &AddStaffCmd, score: &mut Score) -> Result<(), Error> {
1880 let ts = score.settings.time_signature.clone();
1881 let measure_count = score
1882 .parts
1883 .get(cmd.part_index)
1884 .ok_or(Error::PartNotFound(cmd.part_index))?
1885 .staves
1886 .first()
1887 .map_or(0, |s| s.measures.len());
1888 let mut staff = Staff::new(cmd.clef.clone());
1889 for i in 0..measure_count {
1890 let mut m = Measure::empty(ts.numerator, ts.denominator);
1891 m.number = i as u32 + 1;
1892 staff.measures.push(m);
1893 }
1894 score.parts[cmd.part_index].staves.push(staff);
1895 Ok(())
1896}
1897
1898fn apply_delete_staff(cmd: &DeleteStaffCmd, score: &mut Score) -> Result<(), Error> {
1899 let part = score
1900 .parts
1901 .get_mut(cmd.part_index)
1902 .ok_or(Error::PartNotFound(cmd.part_index))?;
1903 if part.staves.len() <= 1 {
1904 return Err(Error::CannotDeleteLastStaff);
1905 }
1906 if cmd.staff_index >= part.staves.len() {
1907 return Err(Error::StaffNotFound(cmd.staff_index));
1908 }
1909 part.staves.remove(cmd.staff_index);
1910 Ok(())
1911}
1912
1913fn trim_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
1914 let mut total = 0.0f64;
1915 let mut cutoff = voice.len();
1916 for (i, n) in voice.iter().enumerate() {
1917 total += n.beats();
1918 if total > max_beats + 1e-9 {
1919 cutoff = i;
1920 break;
1921 }
1922 }
1923 voice.truncate(cutoff);
1924 pad_voice_to_measure(voice, max_beats);
1925}
1926
1927fn pad_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
1928 let mut used: f64 = voice.iter().map(|n| n.beats()).sum();
1929 while max_beats - used > 1e-9 {
1930 let remaining = max_beats - used;
1931 let rest = Note::rest(Duration::whole_filling_beats(remaining));
1932 used += rest.beats();
1933 voice.push(rest);
1934 }
1935}
1936
1937#[cfg(test)]
1938mod tests {
1939 use super::*;
1940 use crate::model::pitch::Step;
1941
1942 fn default_engine_score() -> Score {
1943 let mut s = Score::default();
1944 for part in &mut s.parts {
1945 for staff in &mut part.staves {
1946 for (i, m) in staff.measures.iter_mut().enumerate() {
1947 m.number = i as u32 + 1;
1948 }
1949 }
1950 }
1951 s
1952 }
1953
1954 #[test]
1955 fn add_note_inserts_into_voice() {
1956 let mut score = default_engine_score();
1957 let cmd = Command::AddNote(AddNoteCmd {
1958 part_index: 0,
1959 staff_index: 0,
1960 measure_index: 0,
1961 voice: 0,
1962 position: 0,
1963 pitch: Some(Pitch::new(Step::C, 4)),
1964 duration: Duration::Quarter,
1965 dot_count: 0,
1966 is_rest: false,
1967 tuplet: None,
1968 });
1969 apply_command(&cmd, &mut score).unwrap();
1970 let first = &score.parts[0].staves[0].measures[0].voices[0][0];
1971 assert!(!first.is_rest);
1972 assert_eq!(first.pitches[0].step, Step::C);
1973 }
1974
1975 #[test]
1976 fn set_tempo_updates_score() {
1977 let mut score = default_engine_score();
1978 let cmd = Command::SetTempo(SetTempoCmd { bpm: 160 });
1979 apply_command(&cmd, &mut score).unwrap();
1980 assert_eq!(score.settings.tempo_bpm, 160);
1981 }
1982
1983 #[test]
1984 fn add_measure_increases_count() {
1985 let mut score = default_engine_score();
1986 let before = score.measure_count();
1987 apply_command(
1988 &Command::AddMeasure(AddMeasureCmd { after_index: 0 }),
1989 &mut score,
1990 )
1991 .unwrap();
1992 assert_eq!(score.measure_count(), before + 1);
1993 }
1994
1995 #[test]
1996 fn delete_measure_decreases_count() {
1997 let mut score = default_engine_score();
1998 let before = score.measure_count();
1999 apply_command(
2000 &Command::DeleteMeasure(DeleteMeasureCmd { measure_index: 0 }),
2001 &mut score,
2002 )
2003 .unwrap();
2004 assert_eq!(score.measure_count(), before - 1);
2005 }
2006
2007 #[test]
2008 fn undo_restores_score() {
2009 let mut stack = CommandStack::new(50);
2010 let mut score = default_engine_score();
2011 let before = score.settings.tempo_bpm;
2012 stack
2013 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
2014 .unwrap();
2015 assert_eq!(score.settings.tempo_bpm, 200);
2016 stack.undo(&mut score).unwrap();
2017 assert_eq!(score.settings.tempo_bpm, before);
2018 }
2019
2020 #[test]
2021 fn redo_reapplies_command() {
2022 let mut stack = CommandStack::new(50);
2023 let mut score = default_engine_score();
2024 stack
2025 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
2026 .unwrap();
2027 stack.undo(&mut score).unwrap();
2028 stack.redo(&mut score).unwrap();
2029 assert_eq!(score.settings.tempo_bpm, 200);
2030 }
2031
2032 #[test]
2033 fn undo_nothing_returns_error() {
2034 let mut stack = CommandStack::new(50);
2035 let mut score = default_engine_score();
2036 assert!(stack.undo(&mut score).is_err());
2037 }
2038
2039 #[test]
2040 fn add_part_appends_part() {
2041 let mut score = default_engine_score();
2042 let before = score.parts.len();
2043 apply_command(
2044 &Command::AddPart(AddPartCmd {
2045 name: "Violin".into(),
2046 short_name: "Vln.".into(),
2047 clefs: vec!["Treble".into()],
2048 midi_channel: 0,
2049 midi_program: 0,
2050 }),
2051 &mut score,
2052 )
2053 .unwrap();
2054 assert_eq!(score.parts.len(), before + 1);
2055 }
2056
2057 #[test]
2058 fn delete_part_removes_part() {
2059 let mut score = default_engine_score();
2060 apply_command(
2061 &Command::AddPart(AddPartCmd {
2062 name: "Violin".into(),
2063 short_name: "V.".into(),
2064 clefs: vec!["Treble".into()],
2065 midi_channel: 0,
2066 midi_program: 0,
2067 }),
2068 &mut score,
2069 )
2070 .unwrap();
2071 let before = score.parts.len();
2072 apply_command(
2073 &Command::DeletePart(DeletePartCmd { part_index: 0 }),
2074 &mut score,
2075 )
2076 .unwrap();
2077 assert_eq!(score.parts.len(), before - 1);
2078 }
2079
2080 #[test]
2081 fn delete_part_out_of_range_returns_err() {
2082 let mut score = default_engine_score();
2083 assert!(
2084 apply_command(
2085 &Command::DeletePart(DeletePartCmd { part_index: 99 }),
2086 &mut score
2087 )
2088 .is_err()
2089 );
2090 }
2091
2092 #[test]
2093 fn delete_part_undo_restores_part() {
2094 let mut stack = CommandStack::new(50);
2095 let mut score = default_engine_score();
2096 apply_command(
2097 &Command::AddPart(AddPartCmd {
2098 name: "Violin".into(),
2099 short_name: "V.".into(),
2100 clefs: vec!["Treble".into()],
2101 midi_channel: 0,
2102 midi_program: 0,
2103 }),
2104 &mut score,
2105 )
2106 .unwrap();
2107 let before = score.parts.len();
2108 stack
2109 .execute(
2110 Command::DeletePart(DeletePartCmd { part_index: 0 }),
2111 &mut score,
2112 )
2113 .unwrap();
2114 assert_eq!(score.parts.len(), before - 1);
2115 stack.undo(&mut score).unwrap();
2116 assert_eq!(score.parts.len(), before);
2117 }
2118
2119 #[test]
2120 fn set_metadata_updates_title() {
2121 let mut score = default_engine_score();
2122 apply_command(
2123 &Command::SetMetadata(SetMetadataCmd {
2124 title: Some("New Title".into()),
2125 ..Default::default()
2126 }),
2127 &mut score,
2128 )
2129 .unwrap();
2130 assert_eq!(score.metadata.title, "New Title");
2131 }
2132
2133 #[test]
2134 fn set_metadata_none_fields_skipped() {
2135 let mut score = default_engine_score();
2136 let original_composer = score.metadata.composer.clone();
2137 apply_command(
2138 &Command::SetMetadata(SetMetadataCmd {
2139 title: Some("X".into()),
2140 ..Default::default()
2141 }),
2142 &mut score,
2143 )
2144 .unwrap();
2145 assert_eq!(score.metadata.composer, original_composer);
2146 }
2147
2148 #[test]
2149 fn set_volta_sets_bracket() {
2150 use crate::model::score::VoltaBracket;
2151 let mut score = default_engine_score();
2152 let volta = VoltaBracket {
2153 number: 1,
2154 kind: "begin_end".into(),
2155 };
2156 apply_command(
2157 &Command::SetVolta(SetVoltaCmd {
2158 measure_index: 0,
2159 volta: Some(volta.clone()),
2160 }),
2161 &mut score,
2162 )
2163 .unwrap();
2164 assert!(score.parts[0].staves[0].measures[0].volta.is_some());
2165 }
2166
2167 #[test]
2168 fn set_volta_none_clears_bracket() {
2169 use crate::model::score::VoltaBracket;
2170 let mut score = default_engine_score();
2171 score.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
2172 number: 1,
2173 kind: "begin_end".into(),
2174 });
2175 apply_command(
2176 &Command::SetVolta(SetVoltaCmd {
2177 measure_index: 0,
2178 volta: None,
2179 }),
2180 &mut score,
2181 )
2182 .unwrap();
2183 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
2184 }
2185
2186 #[test]
2187 fn set_volta_undo_restores_old() {
2188 use crate::model::score::VoltaBracket;
2189 let mut stack = CommandStack::new(50);
2190 let mut score = default_engine_score();
2191 stack
2192 .execute(
2193 Command::SetVolta(SetVoltaCmd {
2194 measure_index: 0,
2195 volta: Some(VoltaBracket {
2196 number: 1,
2197 kind: "begin_end".into(),
2198 }),
2199 }),
2200 &mut score,
2201 )
2202 .unwrap();
2203 stack.undo(&mut score).unwrap();
2204 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
2205 }
2206
2207 #[test]
2208 fn set_clef_updates_staff_clef() {
2209 use crate::model::notation::Clef;
2210 let mut score = default_engine_score();
2211 apply_command(
2212 &Command::SetClef(SetClefCmd {
2213 part_index: 0,
2214 staff_index: 0,
2215 clef: Clef::Bass,
2216 }),
2217 &mut score,
2218 )
2219 .unwrap();
2220 assert_eq!(score.parts[0].staves[0].clef, Clef::Bass);
2221 }
2222
2223 #[test]
2224 fn set_clef_out_of_range_returns_err() {
2225 use crate::model::notation::Clef;
2226 let mut score = default_engine_score();
2227 assert!(
2228 apply_command(
2229 &Command::SetClef(SetClefCmd {
2230 part_index: 99,
2231 staff_index: 0,
2232 clef: Clef::Bass,
2233 }),
2234 &mut score
2235 )
2236 .is_err()
2237 );
2238 }
2239
2240 #[test]
2241 fn set_part_name_updates_name() {
2242 let mut score = default_engine_score();
2243 apply_command(
2244 &Command::SetPartName(SetPartNameCmd {
2245 part_index: 0,
2246 name: "Violin".into(),
2247 short_name: "Vln.".into(),
2248 }),
2249 &mut score,
2250 )
2251 .unwrap();
2252 assert_eq!(score.parts[0].name, "Violin");
2253 assert_eq!(score.parts[0].short_name, "Vln.");
2254 }
2255
2256 #[test]
2257 fn set_part_name_undo_restores_old() {
2258 let mut stack = CommandStack::new(50);
2259 let mut score = default_engine_score();
2260 let original = score.parts[0].name.clone();
2261 stack
2262 .execute(
2263 Command::SetPartName(SetPartNameCmd {
2264 part_index: 0,
2265 name: "Flute".into(),
2266 short_name: "Fl.".into(),
2267 }),
2268 &mut score,
2269 )
2270 .unwrap();
2271 stack.undo(&mut score).unwrap();
2272 assert_eq!(score.parts[0].name, original);
2273 }
2274
2275 #[test]
2276 fn set_metadata_undo_restores_old_title() {
2277 let mut stack = CommandStack::new(50);
2278 let mut score = default_engine_score();
2279 let original = score.metadata.title.clone();
2280 stack
2281 .execute(
2282 Command::SetMetadata(SetMetadataCmd {
2283 title: Some("Changed".into()),
2284 ..Default::default()
2285 }),
2286 &mut score,
2287 )
2288 .unwrap();
2289 assert_ne!(score.metadata.title, original);
2290 stack.undo(&mut score).unwrap();
2291 assert_eq!(score.metadata.title, original);
2292 }
2293
2294 #[test]
2295 fn set_midi_instrument_updates_channel_and_program() {
2296 let mut score = default_engine_score();
2297 apply_command(
2298 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
2299 part_index: 0,
2300 midi_channel: 2,
2301 midi_program: 40,
2302 }),
2303 &mut score,
2304 )
2305 .unwrap();
2306 assert_eq!(score.parts[0].midi_channel, 2);
2307 assert_eq!(score.parts[0].midi_program, 40);
2308 }
2309
2310 #[test]
2311 fn set_midi_instrument_clamps_channel_to_15() {
2312 let mut score = default_engine_score();
2313 apply_command(
2314 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
2315 part_index: 0,
2316 midi_channel: 20,
2317 midi_program: 0,
2318 }),
2319 &mut score,
2320 )
2321 .unwrap();
2322 assert_eq!(score.parts[0].midi_channel, 15);
2323 }
2324
2325 #[test]
2326 fn set_midi_instrument_undo_restores_old() {
2327 let mut stack = CommandStack::new(50);
2328 let mut score = default_engine_score();
2329 score.parts[0].midi_channel = 3;
2330 score.parts[0].midi_program = 10;
2331 stack
2332 .execute(
2333 Command::SetMidiInstrument(SetMidiInstrumentCmd {
2334 part_index: 0,
2335 midi_channel: 9,
2336 midi_program: 114,
2337 }),
2338 &mut score,
2339 )
2340 .unwrap();
2341 stack.undo(&mut score).unwrap();
2342 assert_eq!(score.parts[0].midi_channel, 3);
2343 assert_eq!(score.parts[0].midi_program, 10);
2344 }
2345
2346 #[test]
2347 fn set_transpose_updates_staff() {
2348 let mut score = default_engine_score();
2349 apply_command(
2350 &Command::SetTranspose(SetTransposeCmd {
2351 part_index: 0,
2352 staff_index: 0,
2353 semitones: -2,
2354 }),
2355 &mut score,
2356 )
2357 .unwrap();
2358 assert_eq!(score.parts[0].staves[0].transpose_semitones, -2);
2359 }
2360
2361 #[test]
2362 fn set_transpose_out_of_range_returns_err() {
2363 let mut score = default_engine_score();
2364 assert!(
2365 apply_command(
2366 &Command::SetTranspose(SetTransposeCmd {
2367 part_index: 99,
2368 staff_index: 0,
2369 semitones: -2,
2370 }),
2371 &mut score
2372 )
2373 .is_err()
2374 );
2375 }
2376
2377 #[test]
2378 fn set_tempo_at_measure_sets_tempo() {
2379 let mut score = default_engine_score();
2380 apply_command(
2381 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
2382 measure_index: 0,
2383 bpm: Some(80),
2384 }),
2385 &mut score,
2386 )
2387 .unwrap();
2388 assert_eq!(score.parts[0].staves[0].measures[0].tempo, Some(80));
2389 }
2390
2391 #[test]
2392 fn set_tempo_at_measure_none_clears_tempo() {
2393 let mut score = default_engine_score();
2394 score.parts[0].staves[0].measures[0].tempo = Some(120);
2395 apply_command(
2396 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
2397 measure_index: 0,
2398 bpm: None,
2399 }),
2400 &mut score,
2401 )
2402 .unwrap();
2403 assert!(score.parts[0].staves[0].measures[0].tempo.is_none());
2404 }
2405
2406 #[test]
2409 fn batch_execute_two_commands_single_undo() {
2410 let mut stack = CommandStack::new(50);
2411 let mut score = default_engine_score();
2412 let original_bpm = score.settings.tempo_bpm;
2413 stack
2414 .batch_execute(
2415 vec![
2416 Command::SetTempo(SetTempoCmd { bpm: 160 }),
2417 Command::SetTempo(SetTempoCmd { bpm: 180 }),
2418 ],
2419 &mut score,
2420 )
2421 .unwrap();
2422 assert_eq!(score.settings.tempo_bpm, 180);
2423 stack.undo(&mut score).unwrap();
2424 assert_eq!(score.settings.tempo_bpm, original_bpm);
2425 }
2426
2427 #[test]
2428 fn batch_execute_partial_failure_rollback() {
2429 let mut stack = CommandStack::new(50);
2430 let mut score = default_engine_score();
2431 let original_bpm = score.settings.tempo_bpm;
2432 let result = stack.batch_execute(
2433 vec![
2434 Command::SetTempo(SetTempoCmd { bpm: 160 }),
2435 Command::DeleteNote(DeleteNoteCmd {
2436 note_id: "nonexistent".into(),
2437 part_index: 99,
2438 staff_index: 0,
2439 measure_index: 0,
2440 voice: 0,
2441 }),
2442 ],
2443 &mut score,
2444 );
2445 assert!(result.is_err());
2446 assert_eq!(score.settings.tempo_bpm, original_bpm);
2447 }
2448
2449 #[test]
2450 fn batch_execute_empty_is_noop() {
2451 let mut stack = CommandStack::new(50);
2452 let mut score = default_engine_score();
2453 stack.batch_execute(vec![], &mut score).unwrap();
2454 assert!(!stack.can_undo());
2455 }
2456
2457 #[test]
2460 fn batch_label_used_as_command_key() {
2461 let cmd = Command::Batch(BatchCmd {
2462 commands: vec![],
2463 label: Some("ApplyAI".to_string()),
2464 });
2465 assert_eq!(command_key(&cmd), "ApplyAI");
2466 }
2467
2468 #[test]
2469 fn batch_no_label_key_is_batch() {
2470 let cmd = Command::Batch(BatchCmd {
2471 commands: vec![],
2472 label: None,
2473 });
2474 assert_eq!(command_key(&cmd), "Batch");
2475 }
2476
2477 #[test]
2478 fn batch_label_survives_json_roundtrip() {
2479 let cmd = Command::Batch(BatchCmd {
2480 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 120 })],
2481 label: Some("PasteSelection".to_string()),
2482 });
2483 let json = serde_json::to_string(&cmd).unwrap();
2484 let cmd2: Command = serde_json::from_str(&json).unwrap();
2485 assert_eq!(command_key(&cmd2), "PasteSelection");
2486 }
2487
2488 #[test]
2489 fn batch_label_in_undo_key() {
2490 let mut stack = CommandStack::new(50);
2491 let mut score = default_engine_score();
2492 let cmd = Command::Batch(BatchCmd {
2493 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 140 })],
2494 label: Some("ApplyAI".to_string()),
2495 });
2496 stack.execute(cmd, &mut score).unwrap();
2497 assert_eq!(stack.undo_key(), Some("ApplyAI".to_string()));
2498 }
2499
2500 #[test]
2501 fn undo_returns_change_hint() {
2502 use crate::model::change_hint::ChangeScope;
2503 let mut stack = CommandStack::new(50);
2504 let mut score = default_engine_score();
2505 stack
2506 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
2507 .unwrap();
2508 let hint = stack.undo(&mut score).unwrap();
2509 assert_eq!(hint.scope, ChangeScope::Global);
2510 assert!(hint.playback_dirty);
2511 }
2512
2513 #[test]
2514 fn redo_returns_change_hint() {
2515 use crate::model::change_hint::ChangeScope;
2516 let mut stack = CommandStack::new(50);
2517 let mut score = default_engine_score();
2518 stack
2519 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
2520 .unwrap();
2521 stack.undo(&mut score).unwrap();
2522 let hint = stack.redo(&mut score).unwrap();
2523 assert_eq!(hint.scope, ChangeScope::Global);
2524 assert!(hint.playback_dirty);
2525 }
2526
2527 #[test]
2530 fn toggle_slur_sets_start_and_end() {
2531 let mut score = default_engine_score();
2532 let cmd = Command::AddNote(AddNoteCmd {
2533 part_index: 0,
2534 staff_index: 0,
2535 measure_index: 0,
2536 voice: 0,
2537 position: 0,
2538 pitch: Some(Pitch::new(Step::C, 4)),
2539 duration: Duration::Quarter,
2540 dot_count: 0,
2541 is_rest: false,
2542 tuplet: None,
2543 });
2544 apply_command(&cmd, &mut score).unwrap();
2545 apply_command(
2546 &Command::AddNote(AddNoteCmd {
2547 part_index: 0,
2548 staff_index: 0,
2549 measure_index: 0,
2550 voice: 0,
2551 position: 1,
2552 pitch: Some(Pitch::new(Step::D, 4)),
2553 duration: Duration::Quarter,
2554 dot_count: 0,
2555 is_rest: false,
2556 tuplet: None,
2557 }),
2558 &mut score,
2559 )
2560 .unwrap();
2561 let start = NoteAddr {
2562 part: 0,
2563 staff: 0,
2564 measure: 0,
2565 voice: 0,
2566 note: 0,
2567 };
2568 let end = NoteAddr {
2569 part: 0,
2570 staff: 0,
2571 measure: 0,
2572 voice: 0,
2573 note: 1,
2574 };
2575 apply_command(
2576 &Command::ToggleSlur(ToggleSlurCmd {
2577 start: start.clone(),
2578 end: end.clone(),
2579 }),
2580 &mut score,
2581 )
2582 .unwrap();
2583 assert!(score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
2584 assert!(score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
2585 apply_command(
2587 &Command::ToggleSlur(ToggleSlurCmd { start, end }),
2588 &mut score,
2589 )
2590 .unwrap();
2591 assert!(!score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
2592 assert!(!score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
2593 }
2594
2595 #[test]
2598 fn add_staff_appends_staff_with_correct_measure_count() {
2599 let mut score = default_engine_score();
2600 let before = score.parts[0].staves.len();
2601 let measure_count = score.parts[0].staves[0].measures.len();
2602 apply_command(
2603 &Command::AddStaff(AddStaffCmd {
2604 part_index: 0,
2605 clef: Clef::Bass,
2606 }),
2607 &mut score,
2608 )
2609 .unwrap();
2610 assert_eq!(score.parts[0].staves.len(), before + 1);
2611 let new_staff = score.parts[0].staves.last().unwrap();
2612 assert_eq!(new_staff.measures.len(), measure_count);
2613 }
2614
2615 #[test]
2616 fn add_staff_out_of_range_returns_err() {
2617 let mut score = default_engine_score();
2618 let result = apply_command(
2619 &Command::AddStaff(AddStaffCmd {
2620 part_index: 99,
2621 clef: Clef::Treble,
2622 }),
2623 &mut score,
2624 );
2625 assert!(result.is_err());
2626 }
2627
2628 #[test]
2629 fn delete_staff_removes_extra_staff() {
2630 let mut score = default_engine_score();
2631 apply_command(
2632 &Command::AddStaff(AddStaffCmd {
2633 part_index: 0,
2634 clef: Clef::Bass,
2635 }),
2636 &mut score,
2637 )
2638 .unwrap();
2639 assert_eq!(score.parts[0].staves.len(), 2);
2640 apply_command(
2641 &Command::DeleteStaff(DeleteStaffCmd {
2642 part_index: 0,
2643 staff_index: 1,
2644 }),
2645 &mut score,
2646 )
2647 .unwrap();
2648 assert_eq!(score.parts[0].staves.len(), 1);
2649 }
2650
2651 #[test]
2652 fn delete_last_staff_returns_err() {
2653 let mut score = default_engine_score();
2654 assert_eq!(score.parts[0].staves.len(), 1);
2655 let result = apply_command(
2656 &Command::DeleteStaff(DeleteStaffCmd {
2657 part_index: 0,
2658 staff_index: 0,
2659 }),
2660 &mut score,
2661 );
2662 assert!(result.is_err());
2663 }
2664
2665 #[test]
2668 fn set_tuplet_assigns_and_clears() {
2669 use crate::model::notation::TupletInfo;
2670 let mut score = default_engine_score();
2671 apply_command(
2672 &Command::AddNote(AddNoteCmd {
2673 part_index: 0,
2674 staff_index: 0,
2675 measure_index: 0,
2676 voice: 0,
2677 position: 0,
2678 pitch: Some(Pitch::new(Step::C, 4)),
2679 duration: Duration::Quarter,
2680 dot_count: 0,
2681 is_rest: false,
2682 tuplet: None,
2683 }),
2684 &mut score,
2685 )
2686 .unwrap();
2687 let ti = TupletInfo {
2688 actual_notes: 3,
2689 normal_notes: 2,
2690 };
2691 apply_command(
2692 &Command::SetTuplet(SetTupletCmd {
2693 part_index: 0,
2694 staff_index: 0,
2695 measure_index: 0,
2696 voice_index: 0,
2697 note_index: 0,
2698 tuplet: Some(ti.clone()),
2699 }),
2700 &mut score,
2701 )
2702 .unwrap();
2703 assert_eq!(
2704 score.parts[0].staves[0].measures[0].voices[0][0].tuplet,
2705 Some(ti)
2706 );
2707 apply_command(
2708 &Command::SetTuplet(SetTupletCmd {
2709 part_index: 0,
2710 staff_index: 0,
2711 measure_index: 0,
2712 voice_index: 0,
2713 note_index: 0,
2714 tuplet: None,
2715 }),
2716 &mut score,
2717 )
2718 .unwrap();
2719 assert!(
2720 score.parts[0].staves[0].measures[0].voices[0][0]
2721 .tuplet
2722 .is_none()
2723 );
2724 }
2725
2726 #[test]
2729 fn respell_score_cmd_changes_all_pitches() {
2730 use crate::model::pitch::Step;
2731 let mut score = default_engine_score();
2732 apply_command(
2733 &Command::AddNote(AddNoteCmd {
2734 part_index: 0,
2735 staff_index: 0,
2736 measure_index: 0,
2737 voice: 0,
2738 position: 0,
2739 pitch: Some(Pitch::with_alter(Step::C, 4, 1)), duration: Duration::Quarter,
2741 dot_count: 0,
2742 is_rest: false,
2743 tuplet: None,
2744 }),
2745 &mut score,
2746 )
2747 .unwrap();
2748 apply_command(
2749 &Command::RespellScore(RespellScoreCmd { prefer_flat: true }),
2750 &mut score,
2751 )
2752 .unwrap();
2753 let pitch = &score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
2754 assert_eq!(pitch.step, Step::D);
2755 assert_eq!(pitch.alter, -1); }
2757}