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