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