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