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 note = if cmd.is_rest {
1880 let mut n = Note::rest(cmd.duration.clone());
1881 n.dot_count = cmd.dot_count;
1882 n.tuplet = cmd.tuplet.clone();
1883 n
1884 } else {
1885 let pitch = cmd
1886 .pitch
1887 .clone()
1888 .ok_or_else(|| Error::InvalidCommand("pitch required for non-rest note".into()))?;
1889 let mut n = Note::new(pitch, cmd.duration.clone());
1890 n.dot_count = cmd.dot_count;
1891 n.tuplet = cmd.tuplet.clone();
1892 n
1893 };
1894
1895 let pos = {
1896 let voice = score
1897 .parts
1898 .get_mut(cmd.part_index)
1899 .ok_or(Error::PartNotFound(cmd.part_index))?
1900 .staves
1901 .get_mut(cmd.staff_index)
1902 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1903 .measures
1904 .get_mut(cmd.measure_index)
1905 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
1906 .voices
1907 .get_mut(cmd.voice)
1908 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1909 let pos = cmd.position.min(voice.len());
1910 voice.insert(pos, note);
1911 trim_voice_to_measure(voice, ts_beats);
1912 pos
1913 };
1914 remap_spanners(score, |address| {
1915 if same_voice(
1916 address,
1917 cmd.part_index,
1918 cmd.staff_index,
1919 cmd.measure_index,
1920 cmd.voice,
1921 ) && address.note >= pos
1922 {
1923 let mut shifted = address.clone();
1924 shifted.note += 1;
1925 Some(shifted)
1926 } else {
1927 Some(address.clone())
1928 }
1929 });
1930 Ok(())
1931}
1932
1933fn apply_add_pitch(cmd: &AddPitchCmd, score: &mut Score) -> Result<(), Error> {
1934 let voice = score
1935 .parts
1936 .get_mut(cmd.part_index)
1937 .ok_or(Error::PartNotFound(cmd.part_index))?
1938 .staves
1939 .get_mut(cmd.staff_index)
1940 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1941 .measures
1942 .get_mut(cmd.measure_index)
1943 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
1944 .voices
1945 .get_mut(cmd.voice)
1946 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1947 let note = voice
1948 .get_mut(cmd.note_index)
1949 .ok_or(Error::NoteNotFound(cmd.note_index))?;
1950 if note.is_rest {
1951 return Err(Error::InvalidCommand("cannot add pitch to a rest".into()));
1952 }
1953 if !note
1954 .pitches
1955 .iter()
1956 .any(|p| p.step == cmd.pitch.step && p.octave == cmd.pitch.octave)
1957 {
1958 note.pitches.push(cmd.pitch.clone());
1959 }
1960 Ok(())
1961}
1962
1963fn apply_set_duration(cmd: &SetDurationCmd, score: &mut Score) -> Result<(), Error> {
1964 let ts_beats = score.settings.time_signature.total_beats();
1965 {
1966 let voice = score
1967 .parts
1968 .get_mut(cmd.part_index)
1969 .ok_or(Error::PartNotFound(cmd.part_index))?
1970 .staves
1971 .get_mut(cmd.staff_index)
1972 .ok_or(Error::StaffNotFound(cmd.staff_index))?
1973 .measures
1974 .get_mut(cmd.measure_index)
1975 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
1976 .voices
1977 .get_mut(cmd.voice)
1978 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1979 let note = voice
1980 .get_mut(cmd.note_index)
1981 .ok_or(Error::NoteNotFound(cmd.note_index))?;
1982 note.duration = cmd.duration.clone();
1983 note.dot_count = cmd.dot_count;
1984 trim_voice_to_measure(voice, ts_beats);
1985 }
1986 prune_orphaned_spanners(score);
1987 Ok(())
1988}
1989
1990fn apply_delete_note(cmd: &DeleteNoteCmd, score: &mut Score) -> Result<(), Error> {
1991 let ts_beats = score.settings.time_signature.total_beats();
1992 let deleted_position = {
1993 let voice = score
1994 .parts
1995 .get_mut(cmd.part_index)
1996 .ok_or(Error::PartNotFound(cmd.part_index))?
1997 .staves
1998 .get_mut(cmd.staff_index)
1999 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2000 .measures
2001 .get_mut(cmd.measure_index)
2002 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2003 .voices
2004 .get_mut(cmd.voice)
2005 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2006 let position = voice.iter().position(|note| note.id == cmd.note_id);
2007 voice.retain(|note| note.id != cmd.note_id);
2008 pad_voice_to_measure(voice, ts_beats);
2009 position
2010 };
2011 if let Some(position) = deleted_position {
2012 remap_spanners(score, |address| {
2013 if !same_voice(
2014 address,
2015 cmd.part_index,
2016 cmd.staff_index,
2017 cmd.measure_index,
2018 cmd.voice,
2019 ) {
2020 return Some(address.clone());
2021 }
2022 if address.note == position {
2023 None
2024 } else if address.note > position {
2025 let mut shifted = address.clone();
2026 shifted.note -= 1;
2027 Some(shifted)
2028 } else {
2029 Some(address.clone())
2030 }
2031 });
2032 } else {
2033 prune_orphaned_spanners(score);
2034 }
2035 Ok(())
2036}
2037
2038fn apply_add_measure(cmd: &AddMeasureCmd, score: &mut Score) -> Result<(), Error> {
2039 let ts = score.settings.time_signature.clone();
2040 let insert_at = cmd.after_index.saturating_add(1);
2041 for part in &mut score.parts {
2042 for staff in &mut part.staves {
2043 let insert_at = insert_at.min(staff.measures.len());
2044 let mut m = Measure::empty(ts.numerator, ts.denominator);
2045 m.number = insert_at as u32 + 1;
2046 staff.measures.insert(insert_at, m);
2047 for (i, measure) in staff.measures.iter_mut().enumerate() {
2048 measure.number = i as u32 + 1;
2049 }
2050 }
2051 }
2052 remap_spanners(score, |address| {
2053 if address.measure >= insert_at {
2054 let mut shifted = address.clone();
2055 shifted.measure += 1;
2056 Some(shifted)
2057 } else {
2058 Some(address.clone())
2059 }
2060 });
2061 Ok(())
2062}
2063
2064fn apply_delete_measure(cmd: &DeleteMeasureCmd, score: &mut Score) -> Result<(), Error> {
2065 for part in &mut score.parts {
2066 for staff in &mut part.staves {
2067 if cmd.measure_index < staff.measures.len() {
2068 staff.measures.remove(cmd.measure_index);
2069 for (i, m) in staff.measures.iter_mut().enumerate() {
2070 m.number = i as u32 + 1;
2071 }
2072 }
2073 }
2074 }
2075 remap_spanners(score, |address| {
2076 if address.measure == cmd.measure_index {
2077 None
2078 } else if address.measure > cmd.measure_index {
2079 let mut shifted = address.clone();
2080 shifted.measure -= 1;
2081 Some(shifted)
2082 } else {
2083 Some(address.clone())
2084 }
2085 });
2086 Ok(())
2087}
2088
2089fn apply_add_hairpin(cmd: &AddHairpinCmd, score: &mut Score) -> Result<(), Error> {
2090 let voice = score
2091 .parts
2092 .get_mut(cmd.part_index)
2093 .ok_or(Error::PartNotFound(cmd.part_index))?
2094 .staves
2095 .get_mut(cmd.staff_index)
2096 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2097 .measures
2098 .get_mut(cmd.measure_index)
2099 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2100 .voices
2101 .get_mut(cmd.voice)
2102 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2103 if cmd.start_note_idx >= voice.len() {
2104 return Err(Error::NoteNotFound(cmd.start_note_idx));
2105 }
2106 if cmd.end_note_idx >= voice.len() {
2107 return Err(Error::NoteNotFound(cmd.end_note_idx));
2108 }
2109 if cmd.start_note_idx >= cmd.end_note_idx {
2110 return Err(Error::InvalidCommand(
2111 "start_note_idx must be less than end_note_idx".into(),
2112 ));
2113 }
2114 for note in voice
2115 .iter_mut()
2116 .take(cmd.end_note_idx + 1)
2117 .skip(cmd.start_note_idx)
2118 {
2119 note.hairpin_start = None;
2120 note.hairpin_end = false;
2121 }
2122 voice[cmd.start_note_idx].hairpin_start = Some(cmd.kind);
2123 voice[cmd.end_note_idx].hairpin_end = true;
2124 Ok(())
2125}
2126
2127fn apply_add_pedal(cmd: &AddPedalCmd, score: &mut Score) -> Result<(), Error> {
2128 let voice = score
2129 .parts
2130 .get_mut(cmd.part_index)
2131 .ok_or(Error::PartNotFound(cmd.part_index))?
2132 .staves
2133 .get_mut(cmd.staff_index)
2134 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2135 .measures
2136 .get_mut(cmd.measure_index)
2137 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2138 .voices
2139 .get_mut(cmd.voice)
2140 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2141 if cmd.start_note_idx >= voice.len() {
2142 return Err(Error::NoteNotFound(cmd.start_note_idx));
2143 }
2144 if cmd.end_note_idx >= voice.len() {
2145 return Err(Error::NoteNotFound(cmd.end_note_idx));
2146 }
2147 if cmd.start_note_idx >= cmd.end_note_idx {
2148 return Err(Error::InvalidCommand(
2149 "start_note_idx must be less than end_note_idx".into(),
2150 ));
2151 }
2152 for note in voice
2153 .iter_mut()
2154 .take(cmd.end_note_idx + 1)
2155 .skip(cmd.start_note_idx)
2156 {
2157 note.pedal_start = false;
2158 note.pedal_end = false;
2159 }
2160 voice[cmd.start_note_idx].pedal_start = true;
2161 voice[cmd.end_note_idx].pedal_end = true;
2162 Ok(())
2163}
2164
2165fn apply_toggle_tie(cmd: &ToggleTieCmd, score: &mut Score) -> Result<(), Error> {
2166 let current_tie_start = {
2167 let v = score
2168 .parts
2169 .get(cmd.part_index)
2170 .ok_or(Error::PartNotFound(cmd.part_index))?
2171 .staves
2172 .get(cmd.staff_index)
2173 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2174 .measures
2175 .get(cmd.measure_index)
2176 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2177 .voices
2178 .get(cmd.voice)
2179 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2180 v.get(cmd.note_index)
2181 .ok_or(Error::NoteNotFound(cmd.note_index))?
2182 .tie_start
2183 };
2184 let voice_len = score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index]
2185 .voices[cmd.voice]
2186 .len();
2187 let total_measures = score.parts[cmd.part_index].staves[cmd.staff_index]
2188 .measures
2189 .len();
2190
2191 let new_tie = !current_tie_start;
2192 score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index].voices
2193 [cmd.voice][cmd.note_index]
2194 .tie_start = new_tie;
2195
2196 if cmd.note_index + 1 < voice_len {
2197 score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index].voices
2198 [cmd.voice][cmd.note_index + 1]
2199 .tie_end = new_tie;
2200 } else {
2201 let next_mi = cmd.measure_index + 1;
2202 if next_mi < total_measures {
2203 let next_voice = &mut score.parts[cmd.part_index].staves[cmd.staff_index].measures
2204 [next_mi]
2205 .voices[cmd.voice];
2206 if let Some(n) = next_voice.get_mut(0) {
2207 n.tie_end = new_tie;
2208 }
2209 }
2210 }
2211 Ok(())
2212}
2213
2214fn apply_set_dynamic(cmd: &SetDynamicCmd, score: &mut Score) -> Result<(), Error> {
2215 get_note_mut(
2216 score,
2217 cmd.part_index,
2218 cmd.staff_index,
2219 cmd.measure_index,
2220 cmd.voice,
2221 cmd.note_index,
2222 )?
2223 .dynamic = cmd.dynamic.clone();
2224 Ok(())
2225}
2226
2227fn apply_toggle_articulation(cmd: &ToggleArticulationCmd, score: &mut Score) -> Result<(), Error> {
2228 let note = get_note_mut(
2229 score,
2230 cmd.part_index,
2231 cmd.staff_index,
2232 cmd.measure_index,
2233 cmd.voice,
2234 cmd.note_index,
2235 )?;
2236 if let Some(pos) = note
2237 .articulations
2238 .iter()
2239 .position(|a| a == &cmd.articulation)
2240 {
2241 note.articulations.remove(pos);
2242 } else {
2243 note.articulations.push(cmd.articulation.clone());
2244 }
2245 Ok(())
2246}
2247
2248fn apply_set_time_signature(cmd: &SetTimeSignatureCmd, score: &mut Score) -> Result<(), Error> {
2249 if cmd.numerator == 0 || cmd.denominator == 0 {
2250 return Err(Error::InvalidCommand(
2251 "time signature numerator and denominator must be > 0".into(),
2252 ));
2253 }
2254 if ![1u8, 2, 4, 8, 16, 32].contains(&cmd.denominator) {
2255 return Err(Error::InvalidCommand(format!(
2256 "invalid time signature denominator: {}",
2257 cmd.denominator
2258 )));
2259 }
2260 score.settings.time_signature = TimeSignature {
2261 numerator: cmd.numerator,
2262 denominator: cmd.denominator,
2263 };
2264 let max_beats = score.settings.time_signature.total_beats();
2265 for part in &mut score.parts {
2266 for staff in &mut part.staves {
2267 for measure in &mut staff.measures {
2268 for voice in &mut measure.voices {
2269 trim_voice_to_measure(voice, max_beats);
2270 pad_voice_to_measure(voice, max_beats);
2271 }
2272 }
2273 }
2274 }
2275 Ok(())
2276}
2277
2278fn apply_set_barline(cmd: &SetBarlineCmd, score: &mut Score) -> Result<(), Error> {
2279 for part in &mut score.parts {
2280 for staff in &mut part.staves {
2281 let measure = staff
2282 .measures
2283 .get_mut(cmd.measure_index)
2284 .ok_or(Error::MeasureNotFound(cmd.measure_index))?;
2285 match cmd.side.as_str() {
2286 "left" => measure.barline_left = cmd.barline.clone(),
2287 "right" => measure.barline_right = cmd.barline.clone(),
2288 _ => {
2289 return Err(Error::InvalidCommand(format!(
2290 "invalid barline side: '{}'",
2291 cmd.side
2292 )));
2293 }
2294 }
2295 }
2296 }
2297 Ok(())
2298}
2299
2300fn apply_add_part(cmd: &AddPartCmd, score: &mut Score) -> Result<(), Error> {
2301 if cmd.clefs.is_empty() {
2302 return Err(Error::InvalidCommand(
2303 "AddPart requires at least one clef".into(),
2304 ));
2305 }
2306 let measure_count = score.measure_count();
2307 let ts = score.settings.time_signature.clone();
2308 let mut part = Part::new(&cmd.name, &cmd.short_name);
2309 part.midi_channel = cmd.midi_channel.min(15);
2310 part.midi_program = cmd.midi_program;
2311 for clef_str in &cmd.clefs {
2312 let clef = match clef_str.as_str() {
2313 "Bass" => Clef::Bass,
2314 "Alto" => Clef::Alto,
2315 "Tenor" => Clef::Tenor,
2316 "Percussion" => Clef::Percussion,
2317 _ => Clef::Treble,
2318 };
2319 let mut staff = Staff::new(clef);
2320 for i in 0..measure_count {
2321 let mut m = Measure::empty(ts.numerator, ts.denominator);
2322 m.number = i as u32 + 1;
2323 staff.measures.push(m);
2324 }
2325 part.staves.push(staff);
2326 }
2327 score.parts.push(part);
2328 Ok(())
2329}
2330
2331fn apply_set_clef(cmd: &SetClefCmd, score: &mut Score) -> Result<(), Error> {
2332 score
2333 .parts
2334 .get_mut(cmd.part_index)
2335 .ok_or(Error::PartNotFound(cmd.part_index))?
2336 .staves
2337 .get_mut(cmd.staff_index)
2338 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2339 .clef = cmd.clef.clone();
2340 Ok(())
2341}
2342
2343fn apply_set_part_name(cmd: &SetPartNameCmd, score: &mut Score) -> Result<(), Error> {
2344 let part = score
2345 .parts
2346 .get_mut(cmd.part_index)
2347 .ok_or(Error::PartNotFound(cmd.part_index))?;
2348 part.name = cmd.name.clone();
2349 part.short_name = cmd.short_name.clone();
2350 Ok(())
2351}
2352
2353fn apply_delete_part(cmd: &DeletePartCmd, score: &mut Score) -> Result<(), Error> {
2354 if cmd.part_index >= score.parts.len() {
2355 return Err(Error::PartNotFound(cmd.part_index));
2356 }
2357 score.parts.remove(cmd.part_index);
2358 remap_spanners(score, |address| {
2359 if address.part == cmd.part_index {
2360 None
2361 } else if address.part > cmd.part_index {
2362 let mut shifted = address.clone();
2363 shifted.part -= 1;
2364 Some(shifted)
2365 } else {
2366 Some(address.clone())
2367 }
2368 });
2369 Ok(())
2370}
2371
2372fn apply_set_metadata(cmd: &SetMetadataCmd, score: &mut Score) -> Result<(), Error> {
2373 if let Some(v) = &cmd.title {
2374 score.metadata.title = v.clone();
2375 }
2376 if let Some(v) = &cmd.composer {
2377 score.metadata.composer = v.clone();
2378 }
2379 if let Some(v) = &cmd.lyricist {
2380 score.metadata.lyricist = v.clone();
2381 }
2382 if let Some(v) = &cmd.copyright {
2383 score.metadata.copyright = v.clone();
2384 }
2385 if let Some(v) = &cmd.work_number {
2386 score.metadata.work_number = v.clone();
2387 }
2388 if let Some(v) = &cmd.movement_title {
2389 score.metadata.movement_title = v.clone();
2390 }
2391 Ok(())
2392}
2393
2394fn apply_set_midi_instrument(cmd: &SetMidiInstrumentCmd, score: &mut Score) -> Result<(), Error> {
2395 let part = score
2396 .parts
2397 .get_mut(cmd.part_index)
2398 .ok_or(Error::PartNotFound(cmd.part_index))?;
2399 part.midi_channel = cmd.midi_channel.min(15);
2400 part.midi_program = cmd.midi_program;
2401 Ok(())
2402}
2403
2404fn apply_set_transpose(cmd: &SetTransposeCmd, score: &mut Score) -> Result<(), Error> {
2405 score
2406 .parts
2407 .get_mut(cmd.part_index)
2408 .ok_or(Error::PartNotFound(cmd.part_index))?
2409 .staves
2410 .get_mut(cmd.staff_index)
2411 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2412 .transpose_semitones = cmd.semitones;
2413 Ok(())
2414}
2415
2416fn apply_paste_voice(cmd: &PasteVoiceCmd, score: &mut Score) -> Result<(), Error> {
2417 let endpoint_ids = capture_replaced_endpoint_ids(score, |address| {
2418 same_voice(
2419 address,
2420 cmd.part_index,
2421 cmd.staff_index,
2422 cmd.measure_index,
2423 cmd.voice_index,
2424 )
2425 });
2426 {
2427 let voice = score
2428 .parts
2429 .get_mut(cmd.part_index)
2430 .ok_or(Error::PartNotFound(cmd.part_index))?
2431 .staves
2432 .get_mut(cmd.staff_index)
2433 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2434 .measures
2435 .get_mut(cmd.measure_index)
2436 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2437 .voices
2438 .get_mut(cmd.voice_index)
2439 .ok_or(Error::VoiceOutOfRange(cmd.voice_index))?;
2440 *voice = cmd.notes.clone();
2441 }
2442 remap_replaced_spanner_endpoints(score, &endpoint_ids);
2443 Ok(())
2444}
2445
2446fn apply_paste_range(cmd: &PasteRangeCmd, score: &mut Score) -> Result<(), Error> {
2447 if cmd.voice_index >= 4 {
2448 return Err(Error::VoiceOutOfRange(cmd.voice_index));
2449 }
2450 let endpoint_ids = capture_replaced_endpoint_ids(score, |address| {
2451 address.part == cmd.part_index
2452 && address.staff == cmd.staff_index
2453 && address.voice == cmd.voice_index
2454 && address.measure >= cmd.target_measure
2455 && address.measure < cmd.target_measure.saturating_add(cmd.measures.len())
2456 });
2457 let part = score
2458 .parts
2459 .get_mut(cmd.part_index)
2460 .ok_or(Error::PartNotFound(cmd.part_index))?;
2461 let staff = part
2462 .staves
2463 .get_mut(cmd.staff_index)
2464 .ok_or(Error::StaffNotFound(cmd.staff_index))?;
2465 for (offset, notes) in cmd.measures.iter().enumerate() {
2466 let mi = cmd.target_measure + offset;
2467 let measure = staff
2468 .measures
2469 .get_mut(mi)
2470 .ok_or(Error::MeasureNotFound(mi))?;
2471 measure.voices[cmd.voice_index] = notes.clone();
2472 }
2473 remap_replaced_spanner_endpoints(score, &endpoint_ids);
2474 Ok(())
2475}
2476
2477fn apply_toggle_slur(cmd: &ToggleSlurCmd, score: &mut Score) -> Result<(), Error> {
2478 let new_start = !{
2479 score
2480 .parts
2481 .get(cmd.start.part)
2482 .ok_or(Error::PartNotFound(cmd.start.part))?
2483 .staves
2484 .get(cmd.start.staff)
2485 .ok_or(Error::StaffNotFound(cmd.start.staff))?
2486 .measures
2487 .get(cmd.start.measure)
2488 .ok_or(Error::MeasureNotFound(cmd.start.measure))?
2489 .voices
2490 .get(cmd.start.voice)
2491 .ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
2492 .get(cmd.start.note)
2493 .ok_or(Error::NoteNotFound(cmd.start.note))?
2494 .slur_start
2495 };
2496 let new_end = !{
2497 score
2498 .parts
2499 .get(cmd.end.part)
2500 .ok_or(Error::PartNotFound(cmd.end.part))?
2501 .staves
2502 .get(cmd.end.staff)
2503 .ok_or(Error::StaffNotFound(cmd.end.staff))?
2504 .measures
2505 .get(cmd.end.measure)
2506 .ok_or(Error::MeasureNotFound(cmd.end.measure))?
2507 .voices
2508 .get(cmd.end.voice)
2509 .ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
2510 .get(cmd.end.note)
2511 .ok_or(Error::NoteNotFound(cmd.end.note))?
2512 .slur_end
2513 };
2514 score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
2515 [cmd.start.voice][cmd.start.note]
2516 .slur_start = new_start;
2517 score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
2518 [cmd.end.voice][cmd.end.note]
2519 .slur_end = new_end;
2520 Ok(())
2521}
2522
2523fn apply_toggle_trill_line(cmd: &ToggleTrillLineCmd, score: &mut Score) -> Result<(), Error> {
2524 let new_start = !{
2525 score
2526 .parts
2527 .get(cmd.start.part)
2528 .ok_or(Error::PartNotFound(cmd.start.part))?
2529 .staves
2530 .get(cmd.start.staff)
2531 .ok_or(Error::StaffNotFound(cmd.start.staff))?
2532 .measures
2533 .get(cmd.start.measure)
2534 .ok_or(Error::MeasureNotFound(cmd.start.measure))?
2535 .voices
2536 .get(cmd.start.voice)
2537 .ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
2538 .get(cmd.start.note)
2539 .ok_or(Error::NoteNotFound(cmd.start.note))?
2540 .trill_line_start
2541 };
2542 let new_end = !{
2543 score
2544 .parts
2545 .get(cmd.end.part)
2546 .ok_or(Error::PartNotFound(cmd.end.part))?
2547 .staves
2548 .get(cmd.end.staff)
2549 .ok_or(Error::StaffNotFound(cmd.end.staff))?
2550 .measures
2551 .get(cmd.end.measure)
2552 .ok_or(Error::MeasureNotFound(cmd.end.measure))?
2553 .voices
2554 .get(cmd.end.voice)
2555 .ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
2556 .get(cmd.end.note)
2557 .ok_or(Error::NoteNotFound(cmd.end.note))?
2558 .trill_line_end
2559 };
2560 score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
2561 [cmd.start.voice][cmd.start.note]
2562 .trill_line_start = new_start;
2563 score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
2564 [cmd.end.voice][cmd.end.note]
2565 .trill_line_end = new_end;
2566 Ok(())
2567}
2568
2569fn apply_add_staff(cmd: &AddStaffCmd, score: &mut Score) -> Result<(), Error> {
2570 let ts = score.settings.time_signature.clone();
2571 let measure_count = score
2572 .parts
2573 .get(cmd.part_index)
2574 .ok_or(Error::PartNotFound(cmd.part_index))?
2575 .staves
2576 .first()
2577 .map_or(0, |s| s.measures.len());
2578 let mut staff = Staff::new(cmd.clef.clone());
2579 for i in 0..measure_count {
2580 let mut m = Measure::empty(ts.numerator, ts.denominator);
2581 m.number = i as u32 + 1;
2582 staff.measures.push(m);
2583 }
2584 score.parts[cmd.part_index].staves.push(staff);
2585 Ok(())
2586}
2587
2588fn apply_delete_staff(cmd: &DeleteStaffCmd, score: &mut Score) -> Result<(), Error> {
2589 let part = score
2590 .parts
2591 .get_mut(cmd.part_index)
2592 .ok_or(Error::PartNotFound(cmd.part_index))?;
2593 if part.staves.len() <= 1 {
2594 return Err(Error::CannotDeleteLastStaff);
2595 }
2596 if cmd.staff_index >= part.staves.len() {
2597 return Err(Error::StaffNotFound(cmd.staff_index));
2598 }
2599 part.staves.remove(cmd.staff_index);
2600 remap_spanners(score, |address| {
2601 if address.part != cmd.part_index {
2602 Some(address.clone())
2603 } else if address.staff == cmd.staff_index {
2604 None
2605 } else if address.staff > cmd.staff_index {
2606 let mut shifted = address.clone();
2607 shifted.staff -= 1;
2608 Some(shifted)
2609 } else {
2610 Some(address.clone())
2611 }
2612 });
2613 Ok(())
2614}
2615
2616fn same_voice(address: &NoteAddr, part: usize, staff: usize, measure: usize, voice: usize) -> bool {
2618 address.part == part
2619 && address.staff == staff
2620 && address.measure == measure
2621 && address.voice == voice
2622}
2623
2624fn note_at<'a>(score: &'a Score, address: &NoteAddr) -> Option<&'a Note> {
2625 score
2626 .parts
2627 .get(address.part)
2628 .and_then(|part| part.staves.get(address.staff))
2629 .and_then(|staff| staff.measures.get(address.measure))
2630 .and_then(|measure| measure.voices.get(address.voice))
2631 .and_then(|voice| voice.get(address.note))
2632}
2633
2634fn remap_spanners(score: &mut Score, mut remap: impl FnMut(&NoteAddr) -> Option<NoteAddr>) {
2639 let spanners = std::mem::take(&mut score.spanners);
2640 score.spanners = spanners
2641 .into_iter()
2642 .filter_map(|mut spanner| {
2643 spanner.start = remap(&spanner.start)?;
2644 spanner.end = remap(&spanner.end)?;
2645 (note_at(score, &spanner.start).is_some() && note_at(score, &spanner.end).is_some())
2646 .then_some(spanner)
2647 })
2648 .collect();
2649}
2650
2651fn prune_orphaned_spanners(score: &mut Score) {
2652 remap_spanners(score, |address| Some(address.clone()));
2653}
2654
2655fn capture_replaced_endpoint_ids(
2657 score: &Score,
2658 mut replaced: impl FnMut(&NoteAddr) -> bool,
2659) -> Vec<(NoteAddr, String)> {
2660 score
2661 .spanners
2662 .iter()
2663 .flat_map(|spanner| [&spanner.start, &spanner.end])
2664 .filter(|address| replaced(address))
2665 .filter_map(|address| {
2666 note_at(score, address).map(|note| (address.clone(), note.id.clone()))
2667 })
2668 .collect()
2669}
2670
2671fn remap_replaced_spanner_endpoints(score: &mut Score, endpoint_ids: &[(NoteAddr, String)]) {
2674 let mapped: Vec<(NoteAddr, Option<NoteAddr>)> = endpoint_ids
2675 .iter()
2676 .map(|(address, note_id)| {
2677 let matches: Vec<usize> = score
2678 .parts
2679 .get(address.part)
2680 .and_then(|part| part.staves.get(address.staff))
2681 .and_then(|staff| staff.measures.get(address.measure))
2682 .and_then(|measure| measure.voices.get(address.voice))
2683 .map(|voice| {
2684 voice
2685 .iter()
2686 .enumerate()
2687 .filter_map(|(index, note)| (note.id == *note_id).then_some(index))
2688 .collect()
2689 })
2690 .unwrap_or_default();
2691 let replacement = (matches.len() == 1).then(|| NoteAddr {
2692 note: matches[0],
2693 ..address.clone()
2694 });
2695 (address.clone(), replacement)
2696 })
2697 .collect();
2698 remap_spanners(score, |address| {
2699 mapped
2700 .iter()
2701 .find(|(old, _)| old == address)
2702 .map(|(_, replacement)| replacement.clone())
2703 .unwrap_or_else(|| Some(address.clone()))
2704 });
2705}
2706
2707fn trim_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
2708 let mut total = 0.0f64;
2709 let mut cutoff = voice.len();
2710 for (i, n) in voice.iter().enumerate() {
2711 total += n.beats();
2712 if total > max_beats + 1e-9 {
2713 cutoff = i;
2714 break;
2715 }
2716 }
2717 voice.truncate(cutoff);
2718 pad_voice_to_measure(voice, max_beats);
2719}
2720
2721fn pad_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
2722 let mut used: f64 = voice.iter().map(|n| n.beats()).sum();
2723 while max_beats - used > 1e-9 {
2724 let remaining = max_beats - used;
2725 let rest = Note::rest(Duration::whole_filling_beats(remaining));
2726 used += rest.beats();
2727 voice.push(rest);
2728 }
2729}
2730
2731#[cfg(test)]
2732mod tests {
2733 use super::*;
2734 use crate::ScoreEngine;
2735 use crate::model::pitch::Step;
2736
2737 fn default_engine_score() -> Score {
2738 let mut s = Score::default();
2739 for part in &mut s.parts {
2740 for staff in &mut part.staves {
2741 for (i, m) in staff.measures.iter_mut().enumerate() {
2742 m.number = i as u32 + 1;
2743 }
2744 }
2745 }
2746 s
2747 }
2748
2749 fn score_with_typed_spanner() -> Score {
2750 use crate::model::score::{NotationSpanner, NotationSpannerKind};
2751
2752 let mut score = Score::new("Spanner edits", 120, 4, 4, 0, 1);
2753 score.parts[0].staves[0].measures[0].voices[0] = (0..4)
2754 .map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
2755 .collect();
2756 score.spanners.push(NotationSpanner {
2757 id: "typed-glissando".to_string(),
2758 kind: NotationSpannerKind::Glissando,
2759 start: NoteAddr {
2760 part: 0,
2761 staff: 0,
2762 measure: 0,
2763 voice: 0,
2764 note: 1,
2765 },
2766 end: NoteAddr {
2767 part: 0,
2768 staff: 0,
2769 measure: 0,
2770 voice: 0,
2771 note: 2,
2772 },
2773 number: Some(1),
2774 line_type: None,
2775 text: None,
2776 placement: None,
2777 ottava_size: None,
2778 ottava_type: None,
2779 });
2780 score
2781 }
2782
2783 fn insert_note_at(position: usize) -> Command {
2784 Command::AddNote(AddNoteCmd {
2785 part_index: 0,
2786 staff_index: 0,
2787 measure_index: 0,
2788 voice: 0,
2789 position,
2790 pitch: Some(Pitch::new(Step::D, 5)),
2791 duration: Duration::Quarter,
2792 dot_count: 0,
2793 is_rest: false,
2794 tuplet: None,
2795 })
2796 }
2797
2798 #[test]
2799 fn add_note_inserts_into_voice() {
2800 let mut score = default_engine_score();
2801 let cmd = Command::AddNote(AddNoteCmd {
2802 part_index: 0,
2803 staff_index: 0,
2804 measure_index: 0,
2805 voice: 0,
2806 position: 0,
2807 pitch: Some(Pitch::new(Step::C, 4)),
2808 duration: Duration::Quarter,
2809 dot_count: 0,
2810 is_rest: false,
2811 tuplet: None,
2812 });
2813 apply_command(&cmd, &mut score).unwrap();
2814 let first = &score.parts[0].staves[0].measures[0].voices[0][0];
2815 assert!(!first.is_rest);
2816 assert_eq!(first.pitches[0].step, Step::C);
2817 }
2818
2819 #[test]
2820 fn typed_spanners_follow_note_insertions_at_every_relative_position() {
2821 for (position, expected_start, expected_end) in [
2822 (0, 2, 3), (1, 2, 3), (2, 1, 3), (4, 1, 2), (3, 1, 2), ] {
2828 let mut score = score_with_typed_spanner();
2829 apply_command(&insert_note_at(position), &mut score).unwrap();
2830 let span = score.spanners.first().expect("span remains attached");
2831 assert_eq!(span.start.note, expected_start, "position {position}");
2832 assert_eq!(span.end.note, expected_end, "position {position}");
2833 }
2834 }
2835
2836 #[test]
2837 fn deleting_a_typed_spanner_endpoint_removes_the_whole_span_and_undo_redo_is_atomic() {
2838 let mut score = score_with_typed_spanner();
2839 let endpoint_id = score.parts[0].staves[0].measures[0].voices[0][1].id.clone();
2840 let mut stack = CommandStack::new(8);
2841
2842 stack
2843 .execute(
2844 Command::DeleteNote(DeleteNoteCmd {
2845 note_id: endpoint_id,
2846 part_index: 0,
2847 staff_index: 0,
2848 measure_index: 0,
2849 voice: 0,
2850 }),
2851 &mut score,
2852 )
2853 .unwrap();
2854 assert!(score.spanners.is_empty());
2855
2856 stack.undo(&mut score).unwrap();
2857 assert_eq!(score.spanners[0].start.note, 1);
2858 assert_eq!(score.spanners[0].end.note, 2);
2859
2860 stack.redo(&mut score).unwrap();
2861 assert!(score.spanners.is_empty());
2862 }
2863
2864 #[test]
2865 fn batched_structural_edits_remap_typed_spanners_and_restore_them_on_undo() {
2866 let mut score = score_with_typed_spanner();
2867 let mut stack = CommandStack::new(8);
2868 stack
2869 .batch_execute(
2870 vec![
2871 insert_note_at(0),
2872 Command::SetTempo(SetTempoCmd { bpm: 144 }),
2873 ],
2874 &mut score,
2875 )
2876 .unwrap();
2877 assert_eq!(score.spanners[0].start.note, 2);
2878 assert_eq!(score.spanners[0].end.note, 3);
2879
2880 stack.undo(&mut score).unwrap();
2881 assert_eq!(score.spanners[0].start.note, 1);
2882 assert_eq!(score.spanners[0].end.note, 2);
2883
2884 stack.redo(&mut score).unwrap();
2885 assert_eq!(score.spanners[0].start.note, 2);
2886 assert_eq!(score.spanners[0].end.note, 3);
2887 }
2888
2889 #[test]
2890 fn typed_spanners_remap_only_the_edited_endpoint_across_staff_and_voice() {
2891 use crate::model::score::{NotationSpanner, NotationSpannerKind, ScoreTemplate};
2892
2893 let mut score = Score::template(ScoreTemplate::Piano);
2894 for staff in &mut score.parts[0].staves {
2895 let notes: Vec<Note> = (0..4)
2896 .map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
2897 .collect();
2898 staff.measures[0].voices[0] = notes.clone();
2899 staff.measures[0].voices[1] = notes;
2900 }
2901 score.spanners.push(NotationSpanner {
2902 id: "cross-staff".to_string(),
2903 kind: NotationSpannerKind::Slur,
2904 start: NoteAddr {
2905 part: 0,
2906 staff: 0,
2907 measure: 0,
2908 voice: 0,
2909 note: 1,
2910 },
2911 end: NoteAddr {
2912 part: 0,
2913 staff: 1,
2914 measure: 0,
2915 voice: 1,
2916 note: 2,
2917 },
2918 number: Some(2),
2919 line_type: None,
2920 text: None,
2921 placement: None,
2922 ottava_size: None,
2923 ottava_type: None,
2924 });
2925
2926 let mut stack = CommandStack::new(8);
2927 stack.execute(insert_note_at(0), &mut score).unwrap();
2928 let span = &score.spanners[0];
2929 assert_eq!(span.start.note, 2);
2930 assert_eq!(span.end.staff, 1);
2931 assert_eq!(span.end.voice, 1);
2932 assert_eq!(span.end.note, 2);
2933 }
2934
2935 #[test]
2936 fn set_fingerings_keeps_first_legacy_value_in_sync() {
2937 let mut score = default_engine_score();
2938 apply_command(
2939 &Command::AddNote(AddNoteCmd {
2940 part_index: 0,
2941 staff_index: 0,
2942 measure_index: 0,
2943 voice: 0,
2944 position: 0,
2945 pitch: Some(Pitch::new(Step::C, 4)),
2946 duration: Duration::Quarter,
2947 dot_count: 0,
2948 is_rest: false,
2949 tuplet: None,
2950 }),
2951 &mut score,
2952 )
2953 .unwrap();
2954 apply_command(
2955 &Command::SetFingerings(SetFingeringsCmd {
2956 part_index: 0,
2957 staff_index: 0,
2958 measure_index: 0,
2959 voice: 0,
2960 note_index: 0,
2961 fingerings: vec![1, 3, 4],
2962 }),
2963 &mut score,
2964 )
2965 .unwrap();
2966 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
2967 assert_eq!(note.fingerings, vec![1, 3, 4]);
2968 assert_eq!(note.fingering, Some(1));
2969 }
2970
2971 #[test]
2972 fn set_figured_bass_replaces_measure_figures() {
2973 let mut score = default_engine_score();
2974 let figures = vec![FiguredBassFigure {
2975 number: "6".to_string(),
2976 alter: Some("-1".to_string()),
2977 prefix: Some("+".to_string()),
2978 suffix: None,
2979 extender: false,
2980 }];
2981 apply_command(
2982 &Command::SetFiguredBass(SetFiguredBassCmd {
2983 measure_index: 0,
2984 figures: figures.clone(),
2985 }),
2986 &mut score,
2987 )
2988 .unwrap();
2989 assert_eq!(score.parts[0].staves[0].measures[0].figured_bass, figures);
2990 }
2991
2992 #[test]
2993 fn set_measure_text_supports_append_replace_remove_and_undo_redo() {
2994 let mut engine = crate::ScoreEngine::new();
2995 let address = SetMeasureTextCmd {
2996 part_index: 0,
2997 staff_index: 0,
2998 measure_index: 0,
2999 text_index: 0,
3000 text: Some(StyledText {
3001 style: crate::TextStyle::Technique,
3002 text: "dolce".to_string(),
3003 placement: None,
3004 offset_x: None,
3005 offset_y: None,
3006 relative_x: None,
3007 relative_y: None,
3008 }),
3009 };
3010 engine.apply(Command::SetMeasureText(address)).unwrap();
3011 assert_eq!(
3012 engine.score.parts[0].staves[0].measures[0].texts[0],
3013 StyledText {
3014 style: crate::TextStyle::Technique,
3015 text: "dolce".to_string(),
3016 placement: None,
3017 offset_x: None,
3018 offset_y: None,
3019 relative_x: None,
3020 relative_y: None,
3021 }
3022 );
3023
3024 engine
3025 .apply(Command::SetMeasureText(SetMeasureTextCmd {
3026 text_index: 0,
3027 text: Some(StyledText {
3028 style: crate::TextStyle::RehearsalMark,
3029 text: "A".to_string(),
3030 placement: None,
3031 offset_x: None,
3032 offset_y: None,
3033 relative_x: None,
3034 relative_y: None,
3035 }),
3036 ..SetMeasureTextCmd {
3037 part_index: 0,
3038 staff_index: 0,
3039 measure_index: 0,
3040 text_index: 0,
3041 text: None,
3042 }
3043 }))
3044 .unwrap();
3045 assert_eq!(
3046 engine.score.parts[0].staves[0].measures[0].texts[0].style,
3047 crate::TextStyle::RehearsalMark
3048 );
3049
3050 engine
3051 .apply(Command::SetMeasureText(SetMeasureTextCmd {
3052 text_index: 0,
3053 text: None,
3054 ..SetMeasureTextCmd {
3055 part_index: 0,
3056 staff_index: 0,
3057 measure_index: 0,
3058 text_index: 0,
3059 text: None,
3060 }
3061 }))
3062 .unwrap();
3063 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
3064 engine.undo().unwrap();
3065 assert_eq!(engine.score.parts[0].staves[0].measures[0].texts.len(), 1);
3066 engine.redo().unwrap();
3067 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
3068 }
3069
3070 #[test]
3071 fn set_measure_text_rejects_invalid_index_atomically_and_round_trips_json() {
3072 let mut score = default_engine_score();
3073 let before = score.clone();
3074 let command = Command::SetMeasureText(SetMeasureTextCmd {
3075 part_index: 0,
3076 staff_index: 0,
3077 measure_index: 0,
3078 text_index: 2,
3079 text: Some(StyledText {
3080 style: crate::TextStyle::Expression,
3081 text: "espressivo".to_string(),
3082 placement: None,
3083 offset_x: None,
3084 offset_y: None,
3085 relative_x: None,
3086 relative_y: None,
3087 }),
3088 });
3089 let json = serde_json::to_string(&command).unwrap();
3090 let restored: Command = serde_json::from_str(&json).unwrap();
3091 assert_eq!(
3092 serde_json::to_value(&restored).unwrap(),
3093 serde_json::to_value(&command).unwrap()
3094 );
3095 assert!(apply_command(&restored, &mut score).is_err());
3096 assert_eq!(
3097 serde_json::to_value(&score).unwrap(),
3098 serde_json::to_value(&before).unwrap()
3099 );
3100 }
3101
3102 #[test]
3103 fn set_score_text_supports_append_replace_remove_and_undo_redo() {
3104 let mut engine = crate::ScoreEngine::new();
3105 let text = StyledText {
3106 style: crate::TextStyle::Expression,
3107 text: "Title".to_string(),
3108 placement: None,
3109 offset_x: None,
3110 offset_y: None,
3111 relative_x: None,
3112 relative_y: None,
3113 };
3114 engine
3115 .apply(Command::SetScoreText(SetScoreTextCmd {
3116 text_index: 0,
3117 text: Some(text.clone()),
3118 }))
3119 .unwrap();
3120 assert_eq!(engine.score.texts, vec![text.clone()]);
3121
3122 let mut replacement = text.clone();
3123 replacement.text = "Subtitle".to_string();
3124 engine
3125 .apply(Command::SetScoreText(SetScoreTextCmd {
3126 text_index: 0,
3127 text: Some(replacement.clone()),
3128 }))
3129 .unwrap();
3130 assert_eq!(engine.score.texts, vec![replacement]);
3131
3132 engine
3133 .apply(Command::SetScoreText(SetScoreTextCmd {
3134 text_index: 0,
3135 text: None,
3136 }))
3137 .unwrap();
3138 assert!(engine.score.texts.is_empty());
3139 engine.undo().unwrap();
3140 assert_eq!(engine.score.texts.len(), 1);
3141 engine.redo().unwrap();
3142 assert!(engine.score.texts.is_empty());
3143 }
3144
3145 #[test]
3146 fn set_harmony_range_is_undoable_and_json_compatible() {
3147 let mut score = default_engine_score();
3148 score.parts[0].staves[0].measures[0].voices[0] =
3149 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
3150 score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
3151 root: "C".to_owned(),
3152 kind: "major".to_owned(),
3153 bass: None,
3154 placement: None,
3155 extender: true,
3156 harmonic_degree: None,
3157 harmony_function: None,
3158 harmony_type: None,
3159 chord_ref: None,
3160 range_end: None,
3161 degrees: Vec::new(),
3162 });
3163 let mut engine = ScoreEngine::new();
3164 engine.replace_score(score);
3165 let command = Command::SetHarmonyRange(SetHarmonyRangeCmd {
3166 part_index: 0,
3167 staff_index: 0,
3168 measure_index: 0,
3169 voice: 0,
3170 note_index: 0,
3171 end: Some(NoteAddr {
3172 part: 0,
3173 staff: 0,
3174 measure: 0,
3175 voice: 0,
3176 note: 0,
3177 }),
3178 });
3179 let json = serde_json::to_string(&command).unwrap();
3180 let restored: Command = serde_json::from_str(&json).unwrap();
3181 engine.apply(restored).unwrap();
3182 assert!(
3183 engine.score.parts[0].staves[0].measures[0].voices[0][0]
3184 .chord_symbol
3185 .as_ref()
3186 .and_then(|chord| chord.range_end.as_ref())
3187 .is_some()
3188 );
3189 engine.undo().unwrap();
3190 assert!(
3191 engine.score.parts[0].staves[0].measures[0].voices[0][0]
3192 .chord_symbol
3193 .as_ref()
3194 .is_some_and(|chord| chord.range_end.is_none())
3195 );
3196 }
3197
3198 #[test]
3199 fn set_note_placement_is_undoable_and_rejects_non_finite_values() {
3200 let mut score = default_engine_score();
3201 score.parts[0].staves[0].measures[0].voices[0] =
3202 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3203 let mut engine = ScoreEngine::new();
3204 engine.replace_score(score);
3205 let command = Command::SetNotePlacement(SetNotePlacementCmd {
3206 part_index: 0,
3207 staff_index: 0,
3208 measure_index: 0,
3209 voice: 0,
3210 note_index: 0,
3211 offset_x: Some(12.5),
3212 offset_y: Some(-3.0),
3213 relative_x: Some(1.25),
3214 relative_y: Some(-0.5),
3215 });
3216 engine.apply(command).unwrap();
3217 let note = &engine.score.parts[0].staves[0].measures[0].voices[0][0];
3218 assert_eq!(note.offset_x, Some(12.5));
3219 assert_eq!(note.relative_y, Some(-0.5));
3220 engine.undo().unwrap();
3221 assert_eq!(
3222 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
3223 None
3224 );
3225 let invalid = Command::SetNotePlacement(SetNotePlacementCmd {
3226 part_index: 0,
3227 staff_index: 0,
3228 measure_index: 0,
3229 voice: 0,
3230 note_index: 0,
3231 offset_x: Some(f64::NAN),
3232 offset_y: None,
3233 relative_x: None,
3234 relative_y: None,
3235 });
3236 assert!(engine.apply(invalid).is_err());
3237 assert_eq!(
3238 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
3239 None
3240 );
3241 }
3242
3243 #[test]
3244 fn set_guitar_bend_alter_updates_note_and_can_clear() {
3245 let mut score = default_engine_score();
3246 apply_command(
3247 &Command::AddNote(AddNoteCmd {
3248 part_index: 0,
3249 staff_index: 0,
3250 measure_index: 0,
3251 voice: 0,
3252 position: 0,
3253 pitch: Some(Pitch::new(Step::G, 4)),
3254 duration: Duration::Quarter,
3255 dot_count: 0,
3256 is_rest: false,
3257 tuplet: None,
3258 }),
3259 &mut score,
3260 )
3261 .unwrap();
3262 let location = SetGuitarBendAlterCmd {
3263 part_index: 0,
3264 staff_index: 0,
3265 measure_index: 0,
3266 voice: 0,
3267 note_index: 0,
3268 alter_cents: Some(200),
3269 };
3270 apply_command(&Command::SetGuitarBendAlter(location.clone()), &mut score).unwrap();
3271 assert_eq!(
3272 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
3273 Some(200)
3274 );
3275 let mut cleared = location;
3276 cleared.alter_cents = None;
3277 apply_command(&Command::SetGuitarBendAlter(cleared), &mut score).unwrap();
3278 assert_eq!(
3279 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
3280 None
3281 );
3282 }
3283
3284 #[test]
3285 fn set_duration_updates_note_and_preserves_measure_capacity() {
3286 let mut score = default_engine_score();
3287 apply_command(
3288 &Command::AddNote(AddNoteCmd {
3289 part_index: 0,
3290 staff_index: 0,
3291 measure_index: 0,
3292 voice: 0,
3293 position: 0,
3294 pitch: Some(Pitch::new(Step::C, 4)),
3295 duration: Duration::Quarter,
3296 dot_count: 0,
3297 is_rest: false,
3298 tuplet: None,
3299 }),
3300 &mut score,
3301 )
3302 .unwrap();
3303 apply_command(
3304 &Command::SetDuration(SetDurationCmd {
3305 part_index: 0,
3306 staff_index: 0,
3307 measure_index: 0,
3308 voice: 0,
3309 note_index: 0,
3310 duration: Duration::Half,
3311 dot_count: 1,
3312 }),
3313 &mut score,
3314 )
3315 .unwrap();
3316 let voice = &score.parts[0].staves[0].measures[0].voices[0];
3317 assert_eq!(voice[0].duration, Duration::Half);
3318 assert_eq!(voice[0].dot_count, 1);
3319 assert!((voice.iter().map(|note| note.beats()).sum::<f64>() - 4.0).abs() < 1e-9);
3320 }
3321
3322 #[test]
3323 fn set_tempo_updates_score() {
3324 let mut score = default_engine_score();
3325 let cmd = Command::SetTempo(SetTempoCmd { bpm: 160 });
3326 apply_command(&cmd, &mut score).unwrap();
3327 assert_eq!(score.settings.tempo_bpm, 160);
3328 }
3329
3330 #[test]
3331 fn add_measure_increases_count() {
3332 let mut score = default_engine_score();
3333 let before = score.measure_count();
3334 apply_command(
3335 &Command::AddMeasure(AddMeasureCmd { after_index: 0 }),
3336 &mut score,
3337 )
3338 .unwrap();
3339 assert_eq!(score.measure_count(), before + 1);
3340 }
3341
3342 #[test]
3343 fn delete_measure_decreases_count() {
3344 let mut score = default_engine_score();
3345 let before = score.measure_count();
3346 apply_command(
3347 &Command::DeleteMeasure(DeleteMeasureCmd { measure_index: 0 }),
3348 &mut score,
3349 )
3350 .unwrap();
3351 assert_eq!(score.measure_count(), before - 1);
3352 }
3353
3354 #[test]
3355 fn undo_restores_score() {
3356 let mut stack = CommandStack::new(50);
3357 let mut score = default_engine_score();
3358 let before = score.settings.tempo_bpm;
3359 stack
3360 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
3361 .unwrap();
3362 assert_eq!(score.settings.tempo_bpm, 200);
3363 stack.undo(&mut score).unwrap();
3364 assert_eq!(score.settings.tempo_bpm, before);
3365 }
3366
3367 #[test]
3368 fn redo_reapplies_command() {
3369 let mut stack = CommandStack::new(50);
3370 let mut score = default_engine_score();
3371 stack
3372 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
3373 .unwrap();
3374 stack.undo(&mut score).unwrap();
3375 stack.redo(&mut score).unwrap();
3376 assert_eq!(score.settings.tempo_bpm, 200);
3377 }
3378
3379 #[test]
3380 fn undo_nothing_returns_error() {
3381 let mut stack = CommandStack::new(50);
3382 let mut score = default_engine_score();
3383 assert!(stack.undo(&mut score).is_err());
3384 }
3385
3386 #[test]
3387 fn add_part_appends_part() {
3388 let mut score = default_engine_score();
3389 let before = score.parts.len();
3390 apply_command(
3391 &Command::AddPart(AddPartCmd {
3392 name: "Violin".into(),
3393 short_name: "Vln.".into(),
3394 clefs: vec!["Treble".into()],
3395 midi_channel: 0,
3396 midi_program: 0,
3397 }),
3398 &mut score,
3399 )
3400 .unwrap();
3401 assert_eq!(score.parts.len(), before + 1);
3402 }
3403
3404 #[test]
3405 fn delete_part_removes_part() {
3406 let mut score = default_engine_score();
3407 apply_command(
3408 &Command::AddPart(AddPartCmd {
3409 name: "Violin".into(),
3410 short_name: "V.".into(),
3411 clefs: vec!["Treble".into()],
3412 midi_channel: 0,
3413 midi_program: 0,
3414 }),
3415 &mut score,
3416 )
3417 .unwrap();
3418 let before = score.parts.len();
3419 apply_command(
3420 &Command::DeletePart(DeletePartCmd { part_index: 0 }),
3421 &mut score,
3422 )
3423 .unwrap();
3424 assert_eq!(score.parts.len(), before - 1);
3425 }
3426
3427 #[test]
3428 fn delete_part_out_of_range_returns_err() {
3429 let mut score = default_engine_score();
3430 assert!(
3431 apply_command(
3432 &Command::DeletePart(DeletePartCmd { part_index: 99 }),
3433 &mut score
3434 )
3435 .is_err()
3436 );
3437 }
3438
3439 #[test]
3440 fn delete_part_undo_restores_part() {
3441 let mut stack = CommandStack::new(50);
3442 let mut score = default_engine_score();
3443 apply_command(
3444 &Command::AddPart(AddPartCmd {
3445 name: "Violin".into(),
3446 short_name: "V.".into(),
3447 clefs: vec!["Treble".into()],
3448 midi_channel: 0,
3449 midi_program: 0,
3450 }),
3451 &mut score,
3452 )
3453 .unwrap();
3454 let before = score.parts.len();
3455 stack
3456 .execute(
3457 Command::DeletePart(DeletePartCmd { part_index: 0 }),
3458 &mut score,
3459 )
3460 .unwrap();
3461 assert_eq!(score.parts.len(), before - 1);
3462 stack.undo(&mut score).unwrap();
3463 assert_eq!(score.parts.len(), before);
3464 }
3465
3466 #[test]
3467 fn set_metadata_updates_title() {
3468 let mut score = default_engine_score();
3469 apply_command(
3470 &Command::SetMetadata(SetMetadataCmd {
3471 title: Some("New Title".into()),
3472 ..Default::default()
3473 }),
3474 &mut score,
3475 )
3476 .unwrap();
3477 assert_eq!(score.metadata.title, "New Title");
3478 }
3479
3480 #[test]
3481 fn set_metadata_none_fields_skipped() {
3482 let mut score = default_engine_score();
3483 let original_composer = score.metadata.composer.clone();
3484 apply_command(
3485 &Command::SetMetadata(SetMetadataCmd {
3486 title: Some("X".into()),
3487 ..Default::default()
3488 }),
3489 &mut score,
3490 )
3491 .unwrap();
3492 assert_eq!(score.metadata.composer, original_composer);
3493 }
3494
3495 #[test]
3496 fn set_volta_sets_bracket() {
3497 use crate::model::score::VoltaBracket;
3498 let mut score = default_engine_score();
3499 let volta = VoltaBracket {
3500 number: 1,
3501 kind: "begin_end".into(),
3502 };
3503 apply_command(
3504 &Command::SetVolta(SetVoltaCmd {
3505 measure_index: 0,
3506 volta: Some(volta.clone()),
3507 }),
3508 &mut score,
3509 )
3510 .unwrap();
3511 assert!(score.parts[0].staves[0].measures[0].volta.is_some());
3512 }
3513
3514 #[test]
3515 fn set_volta_none_clears_bracket() {
3516 use crate::model::score::VoltaBracket;
3517 let mut score = default_engine_score();
3518 score.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
3519 number: 1,
3520 kind: "begin_end".into(),
3521 });
3522 apply_command(
3523 &Command::SetVolta(SetVoltaCmd {
3524 measure_index: 0,
3525 volta: None,
3526 }),
3527 &mut score,
3528 )
3529 .unwrap();
3530 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
3531 }
3532
3533 #[test]
3534 fn set_volta_undo_restores_old() {
3535 use crate::model::score::VoltaBracket;
3536 let mut stack = CommandStack::new(50);
3537 let mut score = default_engine_score();
3538 stack
3539 .execute(
3540 Command::SetVolta(SetVoltaCmd {
3541 measure_index: 0,
3542 volta: Some(VoltaBracket {
3543 number: 1,
3544 kind: "begin_end".into(),
3545 }),
3546 }),
3547 &mut score,
3548 )
3549 .unwrap();
3550 stack.undo(&mut score).unwrap();
3551 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
3552 }
3553
3554 #[test]
3555 fn set_clef_updates_staff_clef() {
3556 use crate::model::notation::Clef;
3557 let mut score = default_engine_score();
3558 apply_command(
3559 &Command::SetClef(SetClefCmd {
3560 part_index: 0,
3561 staff_index: 0,
3562 clef: Clef::Bass,
3563 }),
3564 &mut score,
3565 )
3566 .unwrap();
3567 assert_eq!(score.parts[0].staves[0].clef, Clef::Bass);
3568 }
3569
3570 #[test]
3571 fn set_clef_out_of_range_returns_err() {
3572 use crate::model::notation::Clef;
3573 let mut score = default_engine_score();
3574 assert!(
3575 apply_command(
3576 &Command::SetClef(SetClefCmd {
3577 part_index: 99,
3578 staff_index: 0,
3579 clef: Clef::Bass,
3580 }),
3581 &mut score
3582 )
3583 .is_err()
3584 );
3585 }
3586
3587 #[test]
3588 fn set_part_name_updates_name() {
3589 let mut score = default_engine_score();
3590 apply_command(
3591 &Command::SetPartName(SetPartNameCmd {
3592 part_index: 0,
3593 name: "Violin".into(),
3594 short_name: "Vln.".into(),
3595 }),
3596 &mut score,
3597 )
3598 .unwrap();
3599 assert_eq!(score.parts[0].name, "Violin");
3600 assert_eq!(score.parts[0].short_name, "Vln.");
3601 }
3602
3603 #[test]
3604 fn set_part_name_undo_restores_old() {
3605 let mut stack = CommandStack::new(50);
3606 let mut score = default_engine_score();
3607 let original = score.parts[0].name.clone();
3608 stack
3609 .execute(
3610 Command::SetPartName(SetPartNameCmd {
3611 part_index: 0,
3612 name: "Flute".into(),
3613 short_name: "Fl.".into(),
3614 }),
3615 &mut score,
3616 )
3617 .unwrap();
3618 stack.undo(&mut score).unwrap();
3619 assert_eq!(score.parts[0].name, original);
3620 }
3621
3622 #[test]
3623 fn set_metadata_undo_restores_old_title() {
3624 let mut stack = CommandStack::new(50);
3625 let mut score = default_engine_score();
3626 let original = score.metadata.title.clone();
3627 stack
3628 .execute(
3629 Command::SetMetadata(SetMetadataCmd {
3630 title: Some("Changed".into()),
3631 ..Default::default()
3632 }),
3633 &mut score,
3634 )
3635 .unwrap();
3636 assert_ne!(score.metadata.title, original);
3637 stack.undo(&mut score).unwrap();
3638 assert_eq!(score.metadata.title, original);
3639 }
3640
3641 #[test]
3642 fn set_midi_instrument_updates_channel_and_program() {
3643 let mut score = default_engine_score();
3644 apply_command(
3645 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
3646 part_index: 0,
3647 midi_channel: 2,
3648 midi_program: 40,
3649 }),
3650 &mut score,
3651 )
3652 .unwrap();
3653 assert_eq!(score.parts[0].midi_channel, 2);
3654 assert_eq!(score.parts[0].midi_program, 40);
3655 }
3656
3657 #[test]
3658 fn set_midi_instrument_clamps_channel_to_15() {
3659 let mut score = default_engine_score();
3660 apply_command(
3661 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
3662 part_index: 0,
3663 midi_channel: 20,
3664 midi_program: 0,
3665 }),
3666 &mut score,
3667 )
3668 .unwrap();
3669 assert_eq!(score.parts[0].midi_channel, 15);
3670 }
3671
3672 #[test]
3673 fn set_midi_instrument_undo_restores_old() {
3674 let mut stack = CommandStack::new(50);
3675 let mut score = default_engine_score();
3676 score.parts[0].midi_channel = 3;
3677 score.parts[0].midi_program = 10;
3678 stack
3679 .execute(
3680 Command::SetMidiInstrument(SetMidiInstrumentCmd {
3681 part_index: 0,
3682 midi_channel: 9,
3683 midi_program: 114,
3684 }),
3685 &mut score,
3686 )
3687 .unwrap();
3688 stack.undo(&mut score).unwrap();
3689 assert_eq!(score.parts[0].midi_channel, 3);
3690 assert_eq!(score.parts[0].midi_program, 10);
3691 }
3692
3693 #[test]
3694 fn set_transpose_updates_staff() {
3695 let mut score = default_engine_score();
3696 apply_command(
3697 &Command::SetTranspose(SetTransposeCmd {
3698 part_index: 0,
3699 staff_index: 0,
3700 semitones: -2,
3701 }),
3702 &mut score,
3703 )
3704 .unwrap();
3705 assert_eq!(score.parts[0].staves[0].transpose_semitones, -2);
3706 }
3707
3708 #[test]
3709 fn set_transpose_out_of_range_returns_err() {
3710 let mut score = default_engine_score();
3711 assert!(
3712 apply_command(
3713 &Command::SetTranspose(SetTransposeCmd {
3714 part_index: 99,
3715 staff_index: 0,
3716 semitones: -2,
3717 }),
3718 &mut score
3719 )
3720 .is_err()
3721 );
3722 }
3723
3724 #[test]
3725 fn set_tempo_at_measure_sets_tempo() {
3726 let mut score = default_engine_score();
3727 apply_command(
3728 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
3729 measure_index: 0,
3730 bpm: Some(80),
3731 }),
3732 &mut score,
3733 )
3734 .unwrap();
3735 assert_eq!(score.parts[0].staves[0].measures[0].tempo, Some(80));
3736 }
3737
3738 #[test]
3739 fn set_tempo_at_measure_none_clears_tempo() {
3740 let mut score = default_engine_score();
3741 score.parts[0].staves[0].measures[0].tempo = Some(120);
3742 apply_command(
3743 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
3744 measure_index: 0,
3745 bpm: None,
3746 }),
3747 &mut score,
3748 )
3749 .unwrap();
3750 assert!(score.parts[0].staves[0].measures[0].tempo.is_none());
3751 }
3752
3753 #[test]
3756 fn batch_execute_two_commands_single_undo() {
3757 let mut stack = CommandStack::new(50);
3758 let mut score = default_engine_score();
3759 let original_bpm = score.settings.tempo_bpm;
3760 stack
3761 .batch_execute(
3762 vec![
3763 Command::SetTempo(SetTempoCmd { bpm: 160 }),
3764 Command::SetTempo(SetTempoCmd { bpm: 180 }),
3765 ],
3766 &mut score,
3767 )
3768 .unwrap();
3769 assert_eq!(score.settings.tempo_bpm, 180);
3770 stack.undo(&mut score).unwrap();
3771 assert_eq!(score.settings.tempo_bpm, original_bpm);
3772 }
3773
3774 #[test]
3775 fn batch_execute_partial_failure_rollback() {
3776 let mut stack = CommandStack::new(50);
3777 let mut score = default_engine_score();
3778 let original_bpm = score.settings.tempo_bpm;
3779 let result = stack.batch_execute(
3780 vec![
3781 Command::SetTempo(SetTempoCmd { bpm: 160 }),
3782 Command::DeleteNote(DeleteNoteCmd {
3783 note_id: "nonexistent".into(),
3784 part_index: 99,
3785 staff_index: 0,
3786 measure_index: 0,
3787 voice: 0,
3788 }),
3789 ],
3790 &mut score,
3791 );
3792 assert!(result.is_err());
3793 assert_eq!(score.settings.tempo_bpm, original_bpm);
3794 }
3795
3796 #[test]
3797 fn execute_rejects_invalid_candidate_without_mutating_or_recording_history() {
3798 let mut stack = CommandStack::new(50);
3799 let mut score = default_engine_score();
3800 score.parts[0].staves[0].tablature = Some(crate::TablatureConfig {
3801 lines: 6,
3802 tuning_midi: vec![40, 45, 50, 55, 59, 64],
3803 capo: 0,
3804 });
3805 stack
3806 .execute(
3807 Command::AddNote(AddNoteCmd {
3808 part_index: 0,
3809 staff_index: 0,
3810 measure_index: 0,
3811 voice: 0,
3812 position: 0,
3813 pitch: Some(Pitch::new(Step::C, 4)),
3814 duration: Duration::Quarter,
3815 dot_count: 0,
3816 is_rest: false,
3817 tuplet: None,
3818 }),
3819 &mut score,
3820 )
3821 .expect("valid pitched note should be accepted");
3822 let original = score.clone();
3823 let result = stack.execute(
3824 Command::SetTabPosition(SetTabPositionCmd {
3825 part_index: 0,
3826 staff_index: 0,
3827 measure_index: 0,
3828 voice: 0,
3829 note_index: 0,
3830 position: Some(crate::TabPosition { string: 7, fret: 0 }),
3831 }),
3832 &mut score,
3833 );
3834 assert!(matches!(result, Err(Error::InvalidScore)));
3835 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
3836 let original_note = &original.parts[0].staves[0].measures[0].voices[0][0];
3837 assert_eq!(note.pitches, original_note.pitches);
3838 assert_eq!(note.duration, original_note.duration);
3839 assert_eq!(note.tab_position, original_note.tab_position);
3840 assert_eq!(note.tab_positions, original_note.tab_positions);
3841 assert!(stack.can_undo());
3842 }
3843
3844 #[test]
3845 fn set_tablature_config_is_undoable_and_json_compatible() {
3846 let mut stack = CommandStack::new(50);
3847 let mut score = default_engine_score();
3848 let config = crate::TablatureConfig {
3849 lines: 6,
3850 tuning_midi: vec![40, 45, 50, 55, 59, 64],
3851 capo: 2,
3852 };
3853 let command = Command::SetTablatureConfig(SetTablatureConfigCmd {
3854 part_index: 0,
3855 staff_index: 0,
3856 config: Some(config.clone()),
3857 });
3858 let json = serde_json::to_string(&command).expect("command should serialize");
3859 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
3860 assert_eq!(command_key(&decoded), "SetTablatureConfig");
3861 stack
3862 .execute(decoded, &mut score)
3863 .expect("config should apply");
3864 assert_eq!(score.parts[0].staves[0].tablature, Some(config));
3865 stack.undo(&mut score).expect("config undo should apply");
3866 assert!(score.parts[0].staves[0].tablature.is_none());
3867 stack.redo(&mut score).expect("config redo should apply");
3868 assert_eq!(
3869 score.parts[0].staves[0]
3870 .tablature
3871 .as_ref()
3872 .map(|tab| tab.capo),
3873 Some(2)
3874 );
3875 }
3876
3877 #[test]
3878 fn batch_execute_empty_is_noop() {
3879 let mut stack = CommandStack::new(50);
3880 let mut score = default_engine_score();
3881 stack.batch_execute(vec![], &mut score).unwrap();
3882 assert!(!stack.can_undo());
3883 }
3884
3885 #[test]
3888 fn batch_label_used_as_command_key() {
3889 let cmd = Command::Batch(BatchCmd {
3890 commands: vec![],
3891 label: Some("ApplyAI".to_string()),
3892 });
3893 assert_eq!(command_key(&cmd), "ApplyAI");
3894 }
3895
3896 #[test]
3897 fn batch_no_label_key_is_batch() {
3898 let cmd = Command::Batch(BatchCmd {
3899 commands: vec![],
3900 label: None,
3901 });
3902 assert_eq!(command_key(&cmd), "Batch");
3903 }
3904
3905 #[test]
3906 fn batch_label_survives_json_roundtrip() {
3907 let cmd = Command::Batch(BatchCmd {
3908 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 120 })],
3909 label: Some("PasteSelection".to_string()),
3910 });
3911 let json = serde_json::to_string(&cmd).unwrap();
3912 let cmd2: Command = serde_json::from_str(&json).unwrap();
3913 assert_eq!(command_key(&cmd2), "PasteSelection");
3914 }
3915
3916 #[test]
3917 fn batch_label_in_undo_key() {
3918 let mut stack = CommandStack::new(50);
3919 let mut score = default_engine_score();
3920 let cmd = Command::Batch(BatchCmd {
3921 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 140 })],
3922 label: Some("ApplyAI".to_string()),
3923 });
3924 stack.execute(cmd, &mut score).unwrap();
3925 assert_eq!(stack.undo_key(), Some("ApplyAI".to_string()));
3926 }
3927
3928 #[test]
3929 fn undo_returns_change_hint() {
3930 use crate::model::change_hint::ChangeScope;
3931 let mut stack = CommandStack::new(50);
3932 let mut score = default_engine_score();
3933 stack
3934 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
3935 .unwrap();
3936 let hint = stack.undo(&mut score).unwrap();
3937 assert_eq!(hint.scope, ChangeScope::Global);
3938 assert!(hint.playback_dirty);
3939 }
3940
3941 #[test]
3942 fn redo_returns_change_hint() {
3943 use crate::model::change_hint::ChangeScope;
3944 let mut stack = CommandStack::new(50);
3945 let mut score = default_engine_score();
3946 stack
3947 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
3948 .unwrap();
3949 stack.undo(&mut score).unwrap();
3950 let hint = stack.redo(&mut score).unwrap();
3951 assert_eq!(hint.scope, ChangeScope::Global);
3952 assert!(hint.playback_dirty);
3953 }
3954
3955 #[test]
3958 fn toggle_slur_sets_start_and_end() {
3959 let mut score = default_engine_score();
3960 let cmd = Command::AddNote(AddNoteCmd {
3961 part_index: 0,
3962 staff_index: 0,
3963 measure_index: 0,
3964 voice: 0,
3965 position: 0,
3966 pitch: Some(Pitch::new(Step::C, 4)),
3967 duration: Duration::Quarter,
3968 dot_count: 0,
3969 is_rest: false,
3970 tuplet: None,
3971 });
3972 apply_command(&cmd, &mut score).unwrap();
3973 apply_command(
3974 &Command::AddNote(AddNoteCmd {
3975 part_index: 0,
3976 staff_index: 0,
3977 measure_index: 0,
3978 voice: 0,
3979 position: 1,
3980 pitch: Some(Pitch::new(Step::D, 4)),
3981 duration: Duration::Quarter,
3982 dot_count: 0,
3983 is_rest: false,
3984 tuplet: None,
3985 }),
3986 &mut score,
3987 )
3988 .unwrap();
3989 let start = NoteAddr {
3990 part: 0,
3991 staff: 0,
3992 measure: 0,
3993 voice: 0,
3994 note: 0,
3995 };
3996 let end = NoteAddr {
3997 part: 0,
3998 staff: 0,
3999 measure: 0,
4000 voice: 0,
4001 note: 1,
4002 };
4003 apply_command(
4004 &Command::ToggleSlur(ToggleSlurCmd {
4005 start: start.clone(),
4006 end: end.clone(),
4007 }),
4008 &mut score,
4009 )
4010 .unwrap();
4011 assert!(score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
4012 assert!(score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
4013 apply_command(
4015 &Command::ToggleSlur(ToggleSlurCmd { start, end }),
4016 &mut score,
4017 )
4018 .unwrap();
4019 assert!(!score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
4020 assert!(!score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
4021 }
4022
4023 #[test]
4026 fn add_staff_appends_staff_with_correct_measure_count() {
4027 let mut score = default_engine_score();
4028 let before = score.parts[0].staves.len();
4029 let measure_count = score.parts[0].staves[0].measures.len();
4030 apply_command(
4031 &Command::AddStaff(AddStaffCmd {
4032 part_index: 0,
4033 clef: Clef::Bass,
4034 }),
4035 &mut score,
4036 )
4037 .unwrap();
4038 assert_eq!(score.parts[0].staves.len(), before + 1);
4039 let new_staff = score.parts[0].staves.last().unwrap();
4040 assert_eq!(new_staff.measures.len(), measure_count);
4041 }
4042
4043 #[test]
4044 fn add_staff_out_of_range_returns_err() {
4045 let mut score = default_engine_score();
4046 let result = apply_command(
4047 &Command::AddStaff(AddStaffCmd {
4048 part_index: 99,
4049 clef: Clef::Treble,
4050 }),
4051 &mut score,
4052 );
4053 assert!(result.is_err());
4054 }
4055
4056 #[test]
4057 fn delete_staff_removes_extra_staff() {
4058 let mut score = default_engine_score();
4059 apply_command(
4060 &Command::AddStaff(AddStaffCmd {
4061 part_index: 0,
4062 clef: Clef::Bass,
4063 }),
4064 &mut score,
4065 )
4066 .unwrap();
4067 assert_eq!(score.parts[0].staves.len(), 2);
4068 apply_command(
4069 &Command::DeleteStaff(DeleteStaffCmd {
4070 part_index: 0,
4071 staff_index: 1,
4072 }),
4073 &mut score,
4074 )
4075 .unwrap();
4076 assert_eq!(score.parts[0].staves.len(), 1);
4077 }
4078
4079 #[test]
4080 fn delete_last_staff_returns_err() {
4081 let mut score = default_engine_score();
4082 assert_eq!(score.parts[0].staves.len(), 1);
4083 let result = apply_command(
4084 &Command::DeleteStaff(DeleteStaffCmd {
4085 part_index: 0,
4086 staff_index: 0,
4087 }),
4088 &mut score,
4089 );
4090 assert!(result.is_err());
4091 }
4092
4093 #[test]
4096 fn set_tuplet_assigns_and_clears() {
4097 use crate::model::notation::TupletInfo;
4098 let mut score = default_engine_score();
4099 apply_command(
4100 &Command::AddNote(AddNoteCmd {
4101 part_index: 0,
4102 staff_index: 0,
4103 measure_index: 0,
4104 voice: 0,
4105 position: 0,
4106 pitch: Some(Pitch::new(Step::C, 4)),
4107 duration: Duration::Quarter,
4108 dot_count: 0,
4109 is_rest: false,
4110 tuplet: None,
4111 }),
4112 &mut score,
4113 )
4114 .unwrap();
4115 let ti = TupletInfo {
4116 actual_notes: 3,
4117 normal_notes: 2,
4118 };
4119 apply_command(
4120 &Command::SetTuplet(SetTupletCmd {
4121 part_index: 0,
4122 staff_index: 0,
4123 measure_index: 0,
4124 voice_index: 0,
4125 note_index: 0,
4126 tuplet: Some(ti.clone()),
4127 }),
4128 &mut score,
4129 )
4130 .unwrap();
4131 assert_eq!(
4132 score.parts[0].staves[0].measures[0].voices[0][0].tuplet,
4133 Some(ti)
4134 );
4135 apply_command(
4136 &Command::SetTuplet(SetTupletCmd {
4137 part_index: 0,
4138 staff_index: 0,
4139 measure_index: 0,
4140 voice_index: 0,
4141 note_index: 0,
4142 tuplet: None,
4143 }),
4144 &mut score,
4145 )
4146 .unwrap();
4147 assert!(
4148 score.parts[0].staves[0].measures[0].voices[0][0]
4149 .tuplet
4150 .is_none()
4151 );
4152 }
4153
4154 #[test]
4157 fn respell_score_cmd_changes_all_pitches() {
4158 use crate::model::pitch::Step;
4159 let mut score = default_engine_score();
4160 apply_command(
4161 &Command::AddNote(AddNoteCmd {
4162 part_index: 0,
4163 staff_index: 0,
4164 measure_index: 0,
4165 voice: 0,
4166 position: 0,
4167 pitch: Some(Pitch::with_alter(Step::C, 4, 1)), duration: Duration::Quarter,
4169 dot_count: 0,
4170 is_rest: false,
4171 tuplet: None,
4172 }),
4173 &mut score,
4174 )
4175 .unwrap();
4176 apply_command(
4177 &Command::RespellScore(RespellScoreCmd { prefer_flat: true }),
4178 &mut score,
4179 )
4180 .unwrap();
4181 let pitch = &score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
4182 assert_eq!(pitch.step, Step::D);
4183 assert_eq!(pitch.alter, -1); }
4185
4186 #[test]
4187 fn typed_spanner_commands_are_atomic_and_undoable() {
4188 use crate::model::score::{NotationSpanner, NotationSpannerKind};
4189
4190 let mut score = default_engine_score();
4191 let address = NoteAddr {
4192 part: 0,
4193 staff: 0,
4194 measure: 0,
4195 voice: 0,
4196 note: 0,
4197 };
4198 let spanner = NotationSpanner {
4199 id: "slur-1".to_string(),
4200 kind: NotationSpannerKind::Slur,
4201 start: address.clone(),
4202 end: address,
4203 number: Some(1),
4204 line_type: Some("dashed".to_string()),
4205 text: None,
4206 placement: Some("above".to_string()),
4207 ottava_size: None,
4208 ottava_type: None,
4209 };
4210 let mut stack = CommandStack::new(8);
4211 stack
4212 .execute(Command::AddSpanner(AddSpannerCmd { spanner }), &mut score)
4213 .unwrap();
4214 assert_eq!(score.spanners.len(), 1);
4215 assert_eq!(stack.undo_key().as_deref(), Some("AddSpanner"));
4216
4217 let before_duplicate = score.clone();
4218 let duplicate = score.spanners[0].clone();
4219 assert!(
4220 stack
4221 .execute(
4222 Command::AddSpanner(AddSpannerCmd { spanner: duplicate }),
4223 &mut score,
4224 )
4225 .is_err()
4226 );
4227 assert_eq!(score.spanners, before_duplicate.spanners);
4228
4229 let mut updated = score.spanners[0].clone();
4230 updated.number = Some(2);
4231 stack
4232 .execute(
4233 Command::UpdateSpanner(UpdateSpannerCmd { spanner: updated }),
4234 &mut score,
4235 )
4236 .unwrap();
4237 assert_eq!(score.spanners[0].number, Some(2));
4238 stack.undo(&mut score).unwrap();
4239 assert_eq!(score.spanners[0].number, Some(1));
4240 stack.redo(&mut score).unwrap();
4241 assert_eq!(score.spanners[0].number, Some(2));
4242
4243 stack
4244 .execute(
4245 Command::RemoveSpanner(RemoveSpannerCmd {
4246 id: "slur-1".to_string(),
4247 }),
4248 &mut score,
4249 )
4250 .unwrap();
4251 assert!(score.spanners.is_empty());
4252 stack.undo(&mut score).unwrap();
4253 assert_eq!(score.spanners.len(), 1);
4254 }
4255}