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