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 }),
2390 };
2391 engine.apply(Command::SetMeasureText(address)).unwrap();
2392 assert_eq!(
2393 engine.score.parts[0].staves[0].measures[0].texts[0],
2394 StyledText {
2395 style: crate::TextStyle::Technique,
2396 text: "dolce".to_string(),
2397 }
2398 );
2399
2400 engine
2401 .apply(Command::SetMeasureText(SetMeasureTextCmd {
2402 text_index: 0,
2403 text: Some(StyledText {
2404 style: crate::TextStyle::RehearsalMark,
2405 text: "A".to_string(),
2406 }),
2407 ..SetMeasureTextCmd {
2408 part_index: 0,
2409 staff_index: 0,
2410 measure_index: 0,
2411 text_index: 0,
2412 text: None,
2413 }
2414 }))
2415 .unwrap();
2416 assert_eq!(
2417 engine.score.parts[0].staves[0].measures[0].texts[0].style,
2418 crate::TextStyle::RehearsalMark
2419 );
2420
2421 engine
2422 .apply(Command::SetMeasureText(SetMeasureTextCmd {
2423 text_index: 0,
2424 text: None,
2425 ..SetMeasureTextCmd {
2426 part_index: 0,
2427 staff_index: 0,
2428 measure_index: 0,
2429 text_index: 0,
2430 text: None,
2431 }
2432 }))
2433 .unwrap();
2434 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
2435 engine.undo().unwrap();
2436 assert_eq!(engine.score.parts[0].staves[0].measures[0].texts.len(), 1);
2437 engine.redo().unwrap();
2438 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
2439 }
2440
2441 #[test]
2442 fn set_measure_text_rejects_invalid_index_atomically_and_round_trips_json() {
2443 let mut score = default_engine_score();
2444 let before = score.clone();
2445 let command = Command::SetMeasureText(SetMeasureTextCmd {
2446 part_index: 0,
2447 staff_index: 0,
2448 measure_index: 0,
2449 text_index: 2,
2450 text: Some(StyledText {
2451 style: crate::TextStyle::Expression,
2452 text: "espressivo".to_string(),
2453 }),
2454 });
2455 let json = serde_json::to_string(&command).unwrap();
2456 let restored: Command = serde_json::from_str(&json).unwrap();
2457 assert_eq!(
2458 serde_json::to_value(&restored).unwrap(),
2459 serde_json::to_value(&command).unwrap()
2460 );
2461 assert!(apply_command(&restored, &mut score).is_err());
2462 assert_eq!(
2463 serde_json::to_value(&score).unwrap(),
2464 serde_json::to_value(&before).unwrap()
2465 );
2466 }
2467
2468 #[test]
2469 fn set_guitar_bend_alter_updates_note_and_can_clear() {
2470 let mut score = default_engine_score();
2471 apply_command(
2472 &Command::AddNote(AddNoteCmd {
2473 part_index: 0,
2474 staff_index: 0,
2475 measure_index: 0,
2476 voice: 0,
2477 position: 0,
2478 pitch: Some(Pitch::new(Step::G, 4)),
2479 duration: Duration::Quarter,
2480 dot_count: 0,
2481 is_rest: false,
2482 tuplet: None,
2483 }),
2484 &mut score,
2485 )
2486 .unwrap();
2487 let location = SetGuitarBendAlterCmd {
2488 part_index: 0,
2489 staff_index: 0,
2490 measure_index: 0,
2491 voice: 0,
2492 note_index: 0,
2493 alter_cents: Some(200),
2494 };
2495 apply_command(&Command::SetGuitarBendAlter(location.clone()), &mut score).unwrap();
2496 assert_eq!(
2497 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
2498 Some(200)
2499 );
2500 let mut cleared = location;
2501 cleared.alter_cents = None;
2502 apply_command(&Command::SetGuitarBendAlter(cleared), &mut score).unwrap();
2503 assert_eq!(
2504 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
2505 None
2506 );
2507 }
2508
2509 #[test]
2510 fn set_duration_updates_note_and_preserves_measure_capacity() {
2511 let mut score = default_engine_score();
2512 apply_command(
2513 &Command::AddNote(AddNoteCmd {
2514 part_index: 0,
2515 staff_index: 0,
2516 measure_index: 0,
2517 voice: 0,
2518 position: 0,
2519 pitch: Some(Pitch::new(Step::C, 4)),
2520 duration: Duration::Quarter,
2521 dot_count: 0,
2522 is_rest: false,
2523 tuplet: None,
2524 }),
2525 &mut score,
2526 )
2527 .unwrap();
2528 apply_command(
2529 &Command::SetDuration(SetDurationCmd {
2530 part_index: 0,
2531 staff_index: 0,
2532 measure_index: 0,
2533 voice: 0,
2534 note_index: 0,
2535 duration: Duration::Half,
2536 dot_count: 1,
2537 }),
2538 &mut score,
2539 )
2540 .unwrap();
2541 let voice = &score.parts[0].staves[0].measures[0].voices[0];
2542 assert_eq!(voice[0].duration, Duration::Half);
2543 assert_eq!(voice[0].dot_count, 1);
2544 assert!((voice.iter().map(|note| note.beats()).sum::<f64>() - 4.0).abs() < 1e-9);
2545 }
2546
2547 #[test]
2548 fn set_tempo_updates_score() {
2549 let mut score = default_engine_score();
2550 let cmd = Command::SetTempo(SetTempoCmd { bpm: 160 });
2551 apply_command(&cmd, &mut score).unwrap();
2552 assert_eq!(score.settings.tempo_bpm, 160);
2553 }
2554
2555 #[test]
2556 fn add_measure_increases_count() {
2557 let mut score = default_engine_score();
2558 let before = score.measure_count();
2559 apply_command(
2560 &Command::AddMeasure(AddMeasureCmd { after_index: 0 }),
2561 &mut score,
2562 )
2563 .unwrap();
2564 assert_eq!(score.measure_count(), before + 1);
2565 }
2566
2567 #[test]
2568 fn delete_measure_decreases_count() {
2569 let mut score = default_engine_score();
2570 let before = score.measure_count();
2571 apply_command(
2572 &Command::DeleteMeasure(DeleteMeasureCmd { measure_index: 0 }),
2573 &mut score,
2574 )
2575 .unwrap();
2576 assert_eq!(score.measure_count(), before - 1);
2577 }
2578
2579 #[test]
2580 fn undo_restores_score() {
2581 let mut stack = CommandStack::new(50);
2582 let mut score = default_engine_score();
2583 let before = score.settings.tempo_bpm;
2584 stack
2585 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
2586 .unwrap();
2587 assert_eq!(score.settings.tempo_bpm, 200);
2588 stack.undo(&mut score).unwrap();
2589 assert_eq!(score.settings.tempo_bpm, before);
2590 }
2591
2592 #[test]
2593 fn redo_reapplies_command() {
2594 let mut stack = CommandStack::new(50);
2595 let mut score = default_engine_score();
2596 stack
2597 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
2598 .unwrap();
2599 stack.undo(&mut score).unwrap();
2600 stack.redo(&mut score).unwrap();
2601 assert_eq!(score.settings.tempo_bpm, 200);
2602 }
2603
2604 #[test]
2605 fn undo_nothing_returns_error() {
2606 let mut stack = CommandStack::new(50);
2607 let mut score = default_engine_score();
2608 assert!(stack.undo(&mut score).is_err());
2609 }
2610
2611 #[test]
2612 fn add_part_appends_part() {
2613 let mut score = default_engine_score();
2614 let before = score.parts.len();
2615 apply_command(
2616 &Command::AddPart(AddPartCmd {
2617 name: "Violin".into(),
2618 short_name: "Vln.".into(),
2619 clefs: vec!["Treble".into()],
2620 midi_channel: 0,
2621 midi_program: 0,
2622 }),
2623 &mut score,
2624 )
2625 .unwrap();
2626 assert_eq!(score.parts.len(), before + 1);
2627 }
2628
2629 #[test]
2630 fn delete_part_removes_part() {
2631 let mut score = default_engine_score();
2632 apply_command(
2633 &Command::AddPart(AddPartCmd {
2634 name: "Violin".into(),
2635 short_name: "V.".into(),
2636 clefs: vec!["Treble".into()],
2637 midi_channel: 0,
2638 midi_program: 0,
2639 }),
2640 &mut score,
2641 )
2642 .unwrap();
2643 let before = score.parts.len();
2644 apply_command(
2645 &Command::DeletePart(DeletePartCmd { part_index: 0 }),
2646 &mut score,
2647 )
2648 .unwrap();
2649 assert_eq!(score.parts.len(), before - 1);
2650 }
2651
2652 #[test]
2653 fn delete_part_out_of_range_returns_err() {
2654 let mut score = default_engine_score();
2655 assert!(
2656 apply_command(
2657 &Command::DeletePart(DeletePartCmd { part_index: 99 }),
2658 &mut score
2659 )
2660 .is_err()
2661 );
2662 }
2663
2664 #[test]
2665 fn delete_part_undo_restores_part() {
2666 let mut stack = CommandStack::new(50);
2667 let mut score = default_engine_score();
2668 apply_command(
2669 &Command::AddPart(AddPartCmd {
2670 name: "Violin".into(),
2671 short_name: "V.".into(),
2672 clefs: vec!["Treble".into()],
2673 midi_channel: 0,
2674 midi_program: 0,
2675 }),
2676 &mut score,
2677 )
2678 .unwrap();
2679 let before = score.parts.len();
2680 stack
2681 .execute(
2682 Command::DeletePart(DeletePartCmd { part_index: 0 }),
2683 &mut score,
2684 )
2685 .unwrap();
2686 assert_eq!(score.parts.len(), before - 1);
2687 stack.undo(&mut score).unwrap();
2688 assert_eq!(score.parts.len(), before);
2689 }
2690
2691 #[test]
2692 fn set_metadata_updates_title() {
2693 let mut score = default_engine_score();
2694 apply_command(
2695 &Command::SetMetadata(SetMetadataCmd {
2696 title: Some("New Title".into()),
2697 ..Default::default()
2698 }),
2699 &mut score,
2700 )
2701 .unwrap();
2702 assert_eq!(score.metadata.title, "New Title");
2703 }
2704
2705 #[test]
2706 fn set_metadata_none_fields_skipped() {
2707 let mut score = default_engine_score();
2708 let original_composer = score.metadata.composer.clone();
2709 apply_command(
2710 &Command::SetMetadata(SetMetadataCmd {
2711 title: Some("X".into()),
2712 ..Default::default()
2713 }),
2714 &mut score,
2715 )
2716 .unwrap();
2717 assert_eq!(score.metadata.composer, original_composer);
2718 }
2719
2720 #[test]
2721 fn set_volta_sets_bracket() {
2722 use crate::model::score::VoltaBracket;
2723 let mut score = default_engine_score();
2724 let volta = VoltaBracket {
2725 number: 1,
2726 kind: "begin_end".into(),
2727 };
2728 apply_command(
2729 &Command::SetVolta(SetVoltaCmd {
2730 measure_index: 0,
2731 volta: Some(volta.clone()),
2732 }),
2733 &mut score,
2734 )
2735 .unwrap();
2736 assert!(score.parts[0].staves[0].measures[0].volta.is_some());
2737 }
2738
2739 #[test]
2740 fn set_volta_none_clears_bracket() {
2741 use crate::model::score::VoltaBracket;
2742 let mut score = default_engine_score();
2743 score.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
2744 number: 1,
2745 kind: "begin_end".into(),
2746 });
2747 apply_command(
2748 &Command::SetVolta(SetVoltaCmd {
2749 measure_index: 0,
2750 volta: None,
2751 }),
2752 &mut score,
2753 )
2754 .unwrap();
2755 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
2756 }
2757
2758 #[test]
2759 fn set_volta_undo_restores_old() {
2760 use crate::model::score::VoltaBracket;
2761 let mut stack = CommandStack::new(50);
2762 let mut score = default_engine_score();
2763 stack
2764 .execute(
2765 Command::SetVolta(SetVoltaCmd {
2766 measure_index: 0,
2767 volta: Some(VoltaBracket {
2768 number: 1,
2769 kind: "begin_end".into(),
2770 }),
2771 }),
2772 &mut score,
2773 )
2774 .unwrap();
2775 stack.undo(&mut score).unwrap();
2776 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
2777 }
2778
2779 #[test]
2780 fn set_clef_updates_staff_clef() {
2781 use crate::model::notation::Clef;
2782 let mut score = default_engine_score();
2783 apply_command(
2784 &Command::SetClef(SetClefCmd {
2785 part_index: 0,
2786 staff_index: 0,
2787 clef: Clef::Bass,
2788 }),
2789 &mut score,
2790 )
2791 .unwrap();
2792 assert_eq!(score.parts[0].staves[0].clef, Clef::Bass);
2793 }
2794
2795 #[test]
2796 fn set_clef_out_of_range_returns_err() {
2797 use crate::model::notation::Clef;
2798 let mut score = default_engine_score();
2799 assert!(
2800 apply_command(
2801 &Command::SetClef(SetClefCmd {
2802 part_index: 99,
2803 staff_index: 0,
2804 clef: Clef::Bass,
2805 }),
2806 &mut score
2807 )
2808 .is_err()
2809 );
2810 }
2811
2812 #[test]
2813 fn set_part_name_updates_name() {
2814 let mut score = default_engine_score();
2815 apply_command(
2816 &Command::SetPartName(SetPartNameCmd {
2817 part_index: 0,
2818 name: "Violin".into(),
2819 short_name: "Vln.".into(),
2820 }),
2821 &mut score,
2822 )
2823 .unwrap();
2824 assert_eq!(score.parts[0].name, "Violin");
2825 assert_eq!(score.parts[0].short_name, "Vln.");
2826 }
2827
2828 #[test]
2829 fn set_part_name_undo_restores_old() {
2830 let mut stack = CommandStack::new(50);
2831 let mut score = default_engine_score();
2832 let original = score.parts[0].name.clone();
2833 stack
2834 .execute(
2835 Command::SetPartName(SetPartNameCmd {
2836 part_index: 0,
2837 name: "Flute".into(),
2838 short_name: "Fl.".into(),
2839 }),
2840 &mut score,
2841 )
2842 .unwrap();
2843 stack.undo(&mut score).unwrap();
2844 assert_eq!(score.parts[0].name, original);
2845 }
2846
2847 #[test]
2848 fn set_metadata_undo_restores_old_title() {
2849 let mut stack = CommandStack::new(50);
2850 let mut score = default_engine_score();
2851 let original = score.metadata.title.clone();
2852 stack
2853 .execute(
2854 Command::SetMetadata(SetMetadataCmd {
2855 title: Some("Changed".into()),
2856 ..Default::default()
2857 }),
2858 &mut score,
2859 )
2860 .unwrap();
2861 assert_ne!(score.metadata.title, original);
2862 stack.undo(&mut score).unwrap();
2863 assert_eq!(score.metadata.title, original);
2864 }
2865
2866 #[test]
2867 fn set_midi_instrument_updates_channel_and_program() {
2868 let mut score = default_engine_score();
2869 apply_command(
2870 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
2871 part_index: 0,
2872 midi_channel: 2,
2873 midi_program: 40,
2874 }),
2875 &mut score,
2876 )
2877 .unwrap();
2878 assert_eq!(score.parts[0].midi_channel, 2);
2879 assert_eq!(score.parts[0].midi_program, 40);
2880 }
2881
2882 #[test]
2883 fn set_midi_instrument_clamps_channel_to_15() {
2884 let mut score = default_engine_score();
2885 apply_command(
2886 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
2887 part_index: 0,
2888 midi_channel: 20,
2889 midi_program: 0,
2890 }),
2891 &mut score,
2892 )
2893 .unwrap();
2894 assert_eq!(score.parts[0].midi_channel, 15);
2895 }
2896
2897 #[test]
2898 fn set_midi_instrument_undo_restores_old() {
2899 let mut stack = CommandStack::new(50);
2900 let mut score = default_engine_score();
2901 score.parts[0].midi_channel = 3;
2902 score.parts[0].midi_program = 10;
2903 stack
2904 .execute(
2905 Command::SetMidiInstrument(SetMidiInstrumentCmd {
2906 part_index: 0,
2907 midi_channel: 9,
2908 midi_program: 114,
2909 }),
2910 &mut score,
2911 )
2912 .unwrap();
2913 stack.undo(&mut score).unwrap();
2914 assert_eq!(score.parts[0].midi_channel, 3);
2915 assert_eq!(score.parts[0].midi_program, 10);
2916 }
2917
2918 #[test]
2919 fn set_transpose_updates_staff() {
2920 let mut score = default_engine_score();
2921 apply_command(
2922 &Command::SetTranspose(SetTransposeCmd {
2923 part_index: 0,
2924 staff_index: 0,
2925 semitones: -2,
2926 }),
2927 &mut score,
2928 )
2929 .unwrap();
2930 assert_eq!(score.parts[0].staves[0].transpose_semitones, -2);
2931 }
2932
2933 #[test]
2934 fn set_transpose_out_of_range_returns_err() {
2935 let mut score = default_engine_score();
2936 assert!(
2937 apply_command(
2938 &Command::SetTranspose(SetTransposeCmd {
2939 part_index: 99,
2940 staff_index: 0,
2941 semitones: -2,
2942 }),
2943 &mut score
2944 )
2945 .is_err()
2946 );
2947 }
2948
2949 #[test]
2950 fn set_tempo_at_measure_sets_tempo() {
2951 let mut score = default_engine_score();
2952 apply_command(
2953 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
2954 measure_index: 0,
2955 bpm: Some(80),
2956 }),
2957 &mut score,
2958 )
2959 .unwrap();
2960 assert_eq!(score.parts[0].staves[0].measures[0].tempo, Some(80));
2961 }
2962
2963 #[test]
2964 fn set_tempo_at_measure_none_clears_tempo() {
2965 let mut score = default_engine_score();
2966 score.parts[0].staves[0].measures[0].tempo = Some(120);
2967 apply_command(
2968 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
2969 measure_index: 0,
2970 bpm: None,
2971 }),
2972 &mut score,
2973 )
2974 .unwrap();
2975 assert!(score.parts[0].staves[0].measures[0].tempo.is_none());
2976 }
2977
2978 #[test]
2981 fn batch_execute_two_commands_single_undo() {
2982 let mut stack = CommandStack::new(50);
2983 let mut score = default_engine_score();
2984 let original_bpm = score.settings.tempo_bpm;
2985 stack
2986 .batch_execute(
2987 vec![
2988 Command::SetTempo(SetTempoCmd { bpm: 160 }),
2989 Command::SetTempo(SetTempoCmd { bpm: 180 }),
2990 ],
2991 &mut score,
2992 )
2993 .unwrap();
2994 assert_eq!(score.settings.tempo_bpm, 180);
2995 stack.undo(&mut score).unwrap();
2996 assert_eq!(score.settings.tempo_bpm, original_bpm);
2997 }
2998
2999 #[test]
3000 fn batch_execute_partial_failure_rollback() {
3001 let mut stack = CommandStack::new(50);
3002 let mut score = default_engine_score();
3003 let original_bpm = score.settings.tempo_bpm;
3004 let result = stack.batch_execute(
3005 vec![
3006 Command::SetTempo(SetTempoCmd { bpm: 160 }),
3007 Command::DeleteNote(DeleteNoteCmd {
3008 note_id: "nonexistent".into(),
3009 part_index: 99,
3010 staff_index: 0,
3011 measure_index: 0,
3012 voice: 0,
3013 }),
3014 ],
3015 &mut score,
3016 );
3017 assert!(result.is_err());
3018 assert_eq!(score.settings.tempo_bpm, original_bpm);
3019 }
3020
3021 #[test]
3022 fn batch_execute_empty_is_noop() {
3023 let mut stack = CommandStack::new(50);
3024 let mut score = default_engine_score();
3025 stack.batch_execute(vec![], &mut score).unwrap();
3026 assert!(!stack.can_undo());
3027 }
3028
3029 #[test]
3032 fn batch_label_used_as_command_key() {
3033 let cmd = Command::Batch(BatchCmd {
3034 commands: vec![],
3035 label: Some("ApplyAI".to_string()),
3036 });
3037 assert_eq!(command_key(&cmd), "ApplyAI");
3038 }
3039
3040 #[test]
3041 fn batch_no_label_key_is_batch() {
3042 let cmd = Command::Batch(BatchCmd {
3043 commands: vec![],
3044 label: None,
3045 });
3046 assert_eq!(command_key(&cmd), "Batch");
3047 }
3048
3049 #[test]
3050 fn batch_label_survives_json_roundtrip() {
3051 let cmd = Command::Batch(BatchCmd {
3052 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 120 })],
3053 label: Some("PasteSelection".to_string()),
3054 });
3055 let json = serde_json::to_string(&cmd).unwrap();
3056 let cmd2: Command = serde_json::from_str(&json).unwrap();
3057 assert_eq!(command_key(&cmd2), "PasteSelection");
3058 }
3059
3060 #[test]
3061 fn batch_label_in_undo_key() {
3062 let mut stack = CommandStack::new(50);
3063 let mut score = default_engine_score();
3064 let cmd = Command::Batch(BatchCmd {
3065 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 140 })],
3066 label: Some("ApplyAI".to_string()),
3067 });
3068 stack.execute(cmd, &mut score).unwrap();
3069 assert_eq!(stack.undo_key(), Some("ApplyAI".to_string()));
3070 }
3071
3072 #[test]
3073 fn undo_returns_change_hint() {
3074 use crate::model::change_hint::ChangeScope;
3075 let mut stack = CommandStack::new(50);
3076 let mut score = default_engine_score();
3077 stack
3078 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
3079 .unwrap();
3080 let hint = stack.undo(&mut score).unwrap();
3081 assert_eq!(hint.scope, ChangeScope::Global);
3082 assert!(hint.playback_dirty);
3083 }
3084
3085 #[test]
3086 fn redo_returns_change_hint() {
3087 use crate::model::change_hint::ChangeScope;
3088 let mut stack = CommandStack::new(50);
3089 let mut score = default_engine_score();
3090 stack
3091 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
3092 .unwrap();
3093 stack.undo(&mut score).unwrap();
3094 let hint = stack.redo(&mut score).unwrap();
3095 assert_eq!(hint.scope, ChangeScope::Global);
3096 assert!(hint.playback_dirty);
3097 }
3098
3099 #[test]
3102 fn toggle_slur_sets_start_and_end() {
3103 let mut score = default_engine_score();
3104 let cmd = Command::AddNote(AddNoteCmd {
3105 part_index: 0,
3106 staff_index: 0,
3107 measure_index: 0,
3108 voice: 0,
3109 position: 0,
3110 pitch: Some(Pitch::new(Step::C, 4)),
3111 duration: Duration::Quarter,
3112 dot_count: 0,
3113 is_rest: false,
3114 tuplet: None,
3115 });
3116 apply_command(&cmd, &mut score).unwrap();
3117 apply_command(
3118 &Command::AddNote(AddNoteCmd {
3119 part_index: 0,
3120 staff_index: 0,
3121 measure_index: 0,
3122 voice: 0,
3123 position: 1,
3124 pitch: Some(Pitch::new(Step::D, 4)),
3125 duration: Duration::Quarter,
3126 dot_count: 0,
3127 is_rest: false,
3128 tuplet: None,
3129 }),
3130 &mut score,
3131 )
3132 .unwrap();
3133 let start = NoteAddr {
3134 part: 0,
3135 staff: 0,
3136 measure: 0,
3137 voice: 0,
3138 note: 0,
3139 };
3140 let end = NoteAddr {
3141 part: 0,
3142 staff: 0,
3143 measure: 0,
3144 voice: 0,
3145 note: 1,
3146 };
3147 apply_command(
3148 &Command::ToggleSlur(ToggleSlurCmd {
3149 start: start.clone(),
3150 end: end.clone(),
3151 }),
3152 &mut score,
3153 )
3154 .unwrap();
3155 assert!(score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
3156 assert!(score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
3157 apply_command(
3159 &Command::ToggleSlur(ToggleSlurCmd { start, end }),
3160 &mut score,
3161 )
3162 .unwrap();
3163 assert!(!score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
3164 assert!(!score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
3165 }
3166
3167 #[test]
3170 fn add_staff_appends_staff_with_correct_measure_count() {
3171 let mut score = default_engine_score();
3172 let before = score.parts[0].staves.len();
3173 let measure_count = score.parts[0].staves[0].measures.len();
3174 apply_command(
3175 &Command::AddStaff(AddStaffCmd {
3176 part_index: 0,
3177 clef: Clef::Bass,
3178 }),
3179 &mut score,
3180 )
3181 .unwrap();
3182 assert_eq!(score.parts[0].staves.len(), before + 1);
3183 let new_staff = score.parts[0].staves.last().unwrap();
3184 assert_eq!(new_staff.measures.len(), measure_count);
3185 }
3186
3187 #[test]
3188 fn add_staff_out_of_range_returns_err() {
3189 let mut score = default_engine_score();
3190 let result = apply_command(
3191 &Command::AddStaff(AddStaffCmd {
3192 part_index: 99,
3193 clef: Clef::Treble,
3194 }),
3195 &mut score,
3196 );
3197 assert!(result.is_err());
3198 }
3199
3200 #[test]
3201 fn delete_staff_removes_extra_staff() {
3202 let mut score = default_engine_score();
3203 apply_command(
3204 &Command::AddStaff(AddStaffCmd {
3205 part_index: 0,
3206 clef: Clef::Bass,
3207 }),
3208 &mut score,
3209 )
3210 .unwrap();
3211 assert_eq!(score.parts[0].staves.len(), 2);
3212 apply_command(
3213 &Command::DeleteStaff(DeleteStaffCmd {
3214 part_index: 0,
3215 staff_index: 1,
3216 }),
3217 &mut score,
3218 )
3219 .unwrap();
3220 assert_eq!(score.parts[0].staves.len(), 1);
3221 }
3222
3223 #[test]
3224 fn delete_last_staff_returns_err() {
3225 let mut score = default_engine_score();
3226 assert_eq!(score.parts[0].staves.len(), 1);
3227 let result = apply_command(
3228 &Command::DeleteStaff(DeleteStaffCmd {
3229 part_index: 0,
3230 staff_index: 0,
3231 }),
3232 &mut score,
3233 );
3234 assert!(result.is_err());
3235 }
3236
3237 #[test]
3240 fn set_tuplet_assigns_and_clears() {
3241 use crate::model::notation::TupletInfo;
3242 let mut score = default_engine_score();
3243 apply_command(
3244 &Command::AddNote(AddNoteCmd {
3245 part_index: 0,
3246 staff_index: 0,
3247 measure_index: 0,
3248 voice: 0,
3249 position: 0,
3250 pitch: Some(Pitch::new(Step::C, 4)),
3251 duration: Duration::Quarter,
3252 dot_count: 0,
3253 is_rest: false,
3254 tuplet: None,
3255 }),
3256 &mut score,
3257 )
3258 .unwrap();
3259 let ti = TupletInfo {
3260 actual_notes: 3,
3261 normal_notes: 2,
3262 };
3263 apply_command(
3264 &Command::SetTuplet(SetTupletCmd {
3265 part_index: 0,
3266 staff_index: 0,
3267 measure_index: 0,
3268 voice_index: 0,
3269 note_index: 0,
3270 tuplet: Some(ti.clone()),
3271 }),
3272 &mut score,
3273 )
3274 .unwrap();
3275 assert_eq!(
3276 score.parts[0].staves[0].measures[0].voices[0][0].tuplet,
3277 Some(ti)
3278 );
3279 apply_command(
3280 &Command::SetTuplet(SetTupletCmd {
3281 part_index: 0,
3282 staff_index: 0,
3283 measure_index: 0,
3284 voice_index: 0,
3285 note_index: 0,
3286 tuplet: None,
3287 }),
3288 &mut score,
3289 )
3290 .unwrap();
3291 assert!(
3292 score.parts[0].staves[0].measures[0].voices[0][0]
3293 .tuplet
3294 .is_none()
3295 );
3296 }
3297
3298 #[test]
3301 fn respell_score_cmd_changes_all_pitches() {
3302 use crate::model::pitch::Step;
3303 let mut score = default_engine_score();
3304 apply_command(
3305 &Command::AddNote(AddNoteCmd {
3306 part_index: 0,
3307 staff_index: 0,
3308 measure_index: 0,
3309 voice: 0,
3310 position: 0,
3311 pitch: Some(Pitch::with_alter(Step::C, 4, 1)), duration: Duration::Quarter,
3313 dot_count: 0,
3314 is_rest: false,
3315 tuplet: None,
3316 }),
3317 &mut score,
3318 )
3319 .unwrap();
3320 apply_command(
3321 &Command::RespellScore(RespellScoreCmd { prefer_flat: true }),
3322 &mut score,
3323 )
3324 .unwrap();
3325 let pitch = &score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
3326 assert_eq!(pitch.step, Step::D);
3327 assert_eq!(pitch.alter, -1); }
3329}