1use serde::{Deserialize, Serialize};
2use super::change_hint::{ChangeHint, ChangeScope};
3use super::score::{Measure, Note, NoteAddr, Part, PartGroup, Score, ScoreTemplate, Staff, respell_score, respell_score_to_key};
4use super::pitch::Pitch;
5use super::duration::Duration;
6use super::notation::{
7 Articulation, Barline, ChordSymbol, Clef, Dynamic, GuitarTechnique, HairpinKind,
8 KeySignature, Lyric, NoteHead, OttavaKind, TimeSignature, TupletInfo,
9};
10use crate::Error;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum Command {
15 AddNote(AddNoteCmd),
16 AddPitch(AddPitchCmd),
17 DeleteNote(DeleteNoteCmd),
18 AddMeasure(AddMeasureCmd),
19 DeleteMeasure(DeleteMeasureCmd),
20 SetTempo(SetTempoCmd),
21 NewScore(NewScoreCmd),
22 AddHairpin(AddHairpinCmd),
23 ToggleTie(ToggleTieCmd),
24 SetDynamic(SetDynamicCmd),
25 ToggleArticulation(ToggleArticulationCmd),
26 SetKeySignature(SetKeySignatureCmd),
27 SetTimeSignature(SetTimeSignatureCmd),
28 SetBarline(SetBarlineCmd),
29 AddPart(AddPartCmd),
30 DeletePart(DeletePartCmd),
31 SetMetadata(SetMetadataCmd),
32 SetRehearsalMark(SetRehearsalMarkCmd),
33 SetNavigationMark(SetNavigationMarkCmd),
34 SetChordSymbol(SetChordSymbolCmd),
35 SetGrace(SetGraceCmd),
36 SetOttava(SetOttavaCmd),
37 SetLyric(SetLyricCmd),
38 SetMultiRest(SetMultiRestCmd),
39 AddPedal(AddPedalCmd),
40 SetVolta(SetVoltaCmd),
41 SetClef(SetClefCmd),
42 SetPartName(SetPartNameCmd),
43 SetMidiInstrument(SetMidiInstrumentCmd),
44 SetTranspose(SetTransposeCmd),
45 SetTempoAtMeasure(SetTempoAtMeasureCmd),
46 PasteVoice(PasteVoiceCmd),
47 PasteRange(PasteRangeCmd),
48 SetSystemBreak(SetSystemBreakCmd),
49 SetPageBreak(SetPageBreakCmd),
50 ToggleSlur(ToggleSlurCmd),
51 AddStaff(AddStaffCmd),
52 DeleteStaff(DeleteStaffCmd),
53 SetTuplet(SetTupletCmd),
54 RespellScore(RespellScoreCmd),
55 RespellScoreToKey(RespellScoreToKeyCmd),
56 SetStem(SetStemCmd),
57 SetArpeggio(SetArpeggioCmd),
58 SetTechniqueText(SetTechniqueTextCmd),
59 SetFingering(SetFingeringCmd),
60 SetStringNumber(SetStringNumberCmd),
61 SetNoteHead(SetNoteHeadCmd),
62 SetCue(SetCueCmd),
63 SetGuitarTechnique(SetGuitarTechniqueCmd),
64 SetExpressionText(SetExpressionTextCmd),
65 ToggleTrillLine(ToggleTrillLineCmd),
66 SetPartGroup(SetPartGroupCmd),
67 Batch(BatchCmd),
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct AddNoteCmd {
72 pub part_index: usize,
73 pub staff_index: usize,
74 pub measure_index: usize,
75 pub voice: usize,
76 pub position: usize,
77 pub pitch: Option<Pitch>,
78 pub duration: Duration,
79 pub dot_count: u8,
80 pub is_rest: bool,
81 #[serde(default)]
82 pub tuplet: Option<TupletInfo>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AddPitchCmd {
87 pub part_index: usize,
88 pub staff_index: usize,
89 pub measure_index: usize,
90 pub voice: usize,
91 pub note_index: usize,
92 pub pitch: Pitch,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct DeleteNoteCmd {
97 pub note_id: String,
98 pub part_index: usize,
99 pub staff_index: usize,
100 pub measure_index: usize,
101 pub voice: usize,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct AddMeasureCmd {
106 pub after_index: usize,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DeleteMeasureCmd {
111 pub measure_index: usize,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SetTempoCmd {
116 pub bpm: u16,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct NewScoreCmd {
121 pub title: String,
122 pub composer: String,
123 pub tempo_bpm: u16,
124 pub time_numerator: u8,
125 pub time_denominator: u8,
126 pub key_fifths: i8,
127 pub measure_count: u32,
128 #[serde(default)]
130 pub template: Option<ScoreTemplate>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct AddHairpinCmd {
135 pub part_index: usize,
136 pub staff_index: usize,
137 pub measure_index: usize,
138 pub voice: usize,
139 pub start_note_idx: usize,
140 pub end_note_idx: usize,
141 pub kind: HairpinKind,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ToggleTieCmd {
146 pub part_index: usize,
147 pub staff_index: usize,
148 pub measure_index: usize,
149 pub voice: usize,
150 pub note_index: usize,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct SetDynamicCmd {
155 pub part_index: usize,
156 pub staff_index: usize,
157 pub measure_index: usize,
158 pub voice: usize,
159 pub note_index: usize,
160 pub dynamic: Option<Dynamic>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct ToggleArticulationCmd {
165 pub part_index: usize,
166 pub staff_index: usize,
167 pub measure_index: usize,
168 pub voice: usize,
169 pub note_index: usize,
170 pub articulation: Articulation,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct SetKeySignatureCmd {
175 pub fifths: i8,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct SetTimeSignatureCmd {
180 pub numerator: u8,
181 pub denominator: u8,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct SetBarlineCmd {
186 pub measure_index: usize,
187 pub side: String,
189 pub barline: Barline,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct AddPartCmd {
194 pub name: String,
195 pub short_name: String,
196 pub clefs: Vec<String>,
198 #[serde(default)]
200 pub midi_channel: u8,
201 #[serde(default)]
203 pub midi_program: u8,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct DeletePartCmd {
208 pub part_index: usize,
209}
210
211#[derive(Debug, Clone, Default, Serialize, Deserialize)]
212pub struct SetMetadataCmd {
213 pub title: Option<String>,
214 pub composer: Option<String>,
215 pub lyricist: Option<String>,
216 pub copyright: Option<String>,
217 pub work_number: Option<String>,
218 pub movement_title: Option<String>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct SetRehearsalMarkCmd {
223 pub measure_index: usize,
224 pub text: Option<String>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct SetNavigationMarkCmd {
229 pub measure_index: usize,
230 pub mark: Option<String>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct SetChordSymbolCmd {
237 pub part_index: usize,
238 pub staff_index: usize,
239 pub measure_index: usize,
240 pub voice: usize,
241 pub note_index: usize,
242 pub chord: Option<ChordSymbol>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct SetGraceCmd {
247 pub part_index: usize,
248 pub staff_index: usize,
249 pub measure_index: usize,
250 pub voice: usize,
251 pub note_index: usize,
252 pub is_grace: bool,
253 pub slash: bool,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct SetOttavaCmd {
259 pub part_index: usize,
260 pub staff_index: usize,
261 pub measure_index: usize,
262 pub voice: usize,
263 pub note_index: usize,
264 pub ottava_start: Option<OttavaKind>,
265 pub ottava_end: bool,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct SetLyricCmd {
270 pub part_index: usize,
271 pub staff_index: usize,
272 pub measure_index: usize,
273 pub voice: usize,
274 pub note_index: usize,
275 pub lyric: Option<Lyric>,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct SetMultiRestCmd {
280 pub measure_index: usize,
281 pub count: Option<u8>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct SetVoltaCmd {
286 pub measure_index: usize,
287 pub volta: Option<super::score::VoltaBracket>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct SetClefCmd {
292 pub part_index: usize,
293 pub staff_index: usize,
294 pub clef: Clef,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct SetPartNameCmd {
299 pub part_index: usize,
300 pub name: String,
301 pub short_name: String,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct SetMidiInstrumentCmd {
306 pub part_index: usize,
307 pub midi_channel: u8,
309 pub midi_program: u8,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct SetTransposeCmd {
315 pub part_index: usize,
316 pub staff_index: usize,
317 pub semitones: i8,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct SetTempoAtMeasureCmd {
323 pub measure_index: usize,
324 pub bpm: Option<u16>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct PasteVoiceCmd {
330 pub part_index: usize,
331 pub staff_index: usize,
332 pub measure_index: usize,
333 pub voice_index: usize,
334 pub notes: Vec<Note>,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct SetSystemBreakCmd {
340 pub measure_index: usize,
341 pub value: bool,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct SetPageBreakCmd {
346 pub measure_index: usize,
347 pub value: bool,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct BatchCmd {
353 pub commands: Vec<Command>,
354 #[serde(default)]
357 pub label: Option<String>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct AddPedalCmd {
362 pub part_index: usize,
363 pub staff_index: usize,
364 pub measure_index: usize,
365 pub voice: usize,
366 pub start_note_idx: usize,
367 pub end_note_idx: usize,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct PasteRangeCmd {
376 pub part_index: usize,
377 pub staff_index: usize,
378 pub voice_index: usize,
379 pub target_measure: usize,
380 pub measures: Vec<Vec<Note>>,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct ToggleSlurCmd {
387 pub start: NoteAddr,
388 pub end: NoteAddr,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct SetPartGroupCmd {
394 pub group: Option<PartGroup>,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct ToggleTrillLineCmd {
401 pub start: NoteAddr,
402 pub end: NoteAddr,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct AddStaffCmd {
411 pub part_index: usize,
412 pub clef: Clef,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct DeleteStaffCmd {
418 pub part_index: usize,
419 pub staff_index: usize,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct SetTupletCmd {
425 pub part_index: usize,
426 pub staff_index: usize,
427 pub measure_index: usize,
428 pub voice_index: usize,
429 pub note_index: usize,
430 pub tuplet: Option<TupletInfo>,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct SetStemCmd {
437 pub part_index: usize,
438 pub staff_index: usize,
439 pub measure_index: usize,
440 pub voice_index: usize,
441 pub note_index: usize,
442 pub stem_up: Option<bool>,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct SetArpeggioCmd {
448 pub part_index: usize,
449 pub staff_index: usize,
450 pub measure_index: usize,
451 pub voice_index: usize,
452 pub note_index: usize,
453 pub direction: Option<bool>,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct SetTechniqueTextCmd {
460 pub part_index: usize,
461 pub staff_index: usize,
462 pub measure_index: usize,
463 pub voice: usize,
464 pub note_index: usize,
465 pub text: Option<String>,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
471pub struct SetFingeringCmd {
472 pub part_index: usize,
473 pub staff_index: usize,
474 pub measure_index: usize,
475 pub voice: usize,
476 pub note_index: usize,
477 pub fingering: Option<u8>,
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct SetStringNumberCmd {
484 pub part_index: usize,
485 pub staff_index: usize,
486 pub measure_index: usize,
487 pub voice: usize,
488 pub note_index: usize,
489 pub string_number: Option<u8>,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct SetGuitarTechniqueCmd {
496 pub part_index: usize,
497 pub staff_index: usize,
498 pub measure_index: usize,
499 pub voice: usize,
500 pub note_index: usize,
501 pub technique: Option<GuitarTechnique>,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
507pub struct SetExpressionTextCmd {
508 pub measure_index: usize,
509 pub text: Option<String>,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct SetCueCmd {
516 pub part_index: usize,
517 pub staff_index: usize,
518 pub measure_index: usize,
519 pub voice: usize,
520 pub note_index: usize,
521 pub is_cue: bool,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct SetNoteHeadCmd {
527 pub part_index: usize,
528 pub staff_index: usize,
529 pub measure_index: usize,
530 pub voice: usize,
531 pub note_index: usize,
532 pub note_head: NoteHead,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct RespellScoreCmd {
538 pub prefer_flat: bool,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct RespellScoreToKeyCmd {}
544
545struct UndoEntry {
546 command: Command,
547 snapshot: Score,
548}
549
550pub struct CommandStack {
551 history: Vec<UndoEntry>,
552 future: Vec<(Command, Score)>,
553 max_depth: usize,
554}
555
556impl CommandStack {
557 pub fn new(max_depth: usize) -> Self {
558 Self { history: Vec::new(), future: Vec::new(), max_depth }
559 }
560
561 pub fn can_undo(&self) -> bool {
562 !self.history.is_empty()
563 }
564
565 pub fn can_redo(&self) -> bool {
566 !self.future.is_empty()
567 }
568
569 pub fn execute(&mut self, cmd: Command, score: &mut Score) -> Result<(), Error> {
570 let snapshot = score.clone();
571 apply_command(&cmd, score)?;
572 self.history.push(UndoEntry { command: cmd, snapshot });
573 self.future.clear();
574 if self.history.len() > self.max_depth {
575 self.history.remove(0);
576 }
577 Ok(())
578 }
579
580 pub fn undo(&mut self, score: &mut Score) -> Result<ChangeHint, Error> {
581 let entry = self.history.pop().ok_or(Error::NothingToUndo)?;
582 let hint = command_hint(&entry.command);
583 let post_snapshot = score.clone();
584 *score = entry.snapshot;
585 self.future.push((entry.command, post_snapshot));
586 if self.future.len() > self.max_depth {
587 self.future.remove(0);
588 }
589 Ok(hint)
590 }
591
592 pub fn redo(&mut self, score: &mut Score) -> Result<ChangeHint, Error> {
593 let (cmd, post) = self.future.pop().ok_or(Error::NothingToRedo)?;
594 let hint = command_hint(&cmd);
595 let snapshot = score.clone();
596 *score = post;
597 self.history.push(UndoEntry { command: cmd, snapshot });
598 Ok(hint)
599 }
600
601 pub fn history_commands(&self) -> Vec<Command> {
604 self.history.iter().map(|e| e.command.clone()).collect()
605 }
606
607 pub fn undo_label(&self) -> Option<String> {
609 self.history.last().map(|e| command_label(&e.command))
610 }
611
612 pub fn redo_label(&self) -> Option<String> {
614 self.future.last().map(|(cmd, _)| command_label(cmd))
615 }
616
617 pub fn undo_key(&self) -> Option<String> {
619 self.history.last().map(|e| command_key(&e.command))
620 }
621
622 pub fn redo_key(&self) -> Option<String> {
624 self.future.last().map(|(cmd, _)| command_key(cmd))
625 }
626
627 pub fn batch_execute(&mut self, cmds: Vec<Command>, score: &mut Score) -> Result<(), Error> {
629 if cmds.is_empty() { return Ok(()); }
630 let snapshot = score.clone();
631 for cmd in &cmds {
632 if let Err(e) = apply_command(cmd, score) {
633 *score = snapshot;
634 return Err(e);
635 }
636 }
637 self.history.push(UndoEntry {
638 command: Command::Batch(BatchCmd { commands: cmds, label: None }),
639 snapshot,
640 });
641 self.future.clear();
642 if self.history.len() > self.max_depth { self.history.remove(0); }
643 Ok(())
644 }
645
646 pub fn batch_execute_labeled(
650 &mut self, cmds: Vec<Command>, label: String, score: &mut Score,
651 ) -> Result<(), Error> {
652 if cmds.is_empty() { return Ok(()); }
653 let snapshot = score.clone();
654 for cmd in &cmds {
655 if let Err(e) = apply_command(cmd, score) {
656 *score = snapshot;
657 return Err(e);
658 }
659 }
660 self.history.push(UndoEntry {
661 command: Command::Batch(BatchCmd { commands: cmds, label: Some(label) }),
662 snapshot,
663 });
664 self.future.clear();
665 if self.history.len() > self.max_depth { self.history.remove(0); }
666 Ok(())
667 }
668}
669
670pub fn command_hint(cmd: &Command) -> ChangeHint {
673 use ChangeScope::*;
674 macro_rules! hint {
675 ($scope:expr, $layout:expr, $playback:expr) => {
676 ChangeHint { scope: $scope, layout_dirty: $layout, playback_dirty: $playback }
677 };
678 }
679 macro_rules! meas {
680 ($c:expr) => {
681 Measures { part: $c.part_index, staff: $c.staff_index,
682 start: $c.measure_index, end: $c.measure_index + 1 }
683 };
684 }
685 match cmd {
686 Command::NewScore(_) | Command::AddPart(_) | Command::DeletePart(_)
688 | Command::AddMeasure(_) | Command::DeleteMeasure(_)
689 => hint!(Global, true, true),
690
691 Command::SetTempo(_)
692 => hint!(Global, false, true),
693
694 Command::SetMetadata(_)
695 => hint!(Global, false, false),
696
697 Command::SetKeySignature(_)
698 => hint!(Global, true, false),
699
700 Command::SetTimeSignature(_)
701 => hint!(Global, true, true),
702
703 Command::SetBarline(_) | Command::SetVolta(_)
704 | Command::SetRehearsalMark(_) | Command::SetNavigationMark(_)
705 | Command::SetExpressionText(_)
706 => hint!(Global, false, false),
707
708 Command::SetMultiRest(_)
709 => hint!(Global, true, false),
710
711 Command::SetTempoAtMeasure(_)
712 => hint!(Global, false, true),
713
714 Command::SetPartName(c)
716 => hint!(Part(c.part_index), false, false),
717
718 Command::SetMidiInstrument(c)
719 => hint!(Part(c.part_index), false, true),
720
721 Command::SetTranspose(c)
722 => hint!(Part(c.part_index), false, true),
723
724 Command::SetClef(c)
725 => hint!(Part(c.part_index), true, false),
726
727 Command::AddNote(c) => hint!(meas!(c), false, true),
729 Command::AddPitch(c) => hint!(meas!(c), false, true),
730 Command::DeleteNote(c) => hint!(meas!(c), false, true),
731 Command::PasteVoice(c) => hint!(meas!(c), false, true),
732 Command::PasteRange(c) => hint!(
733 Measures { part: c.part_index, staff: c.staff_index,
734 start: c.target_measure,
735 end: c.target_measure + c.measures.len() },
736 false, true
737 ),
738 Command::AddHairpin(c) => hint!(meas!(c), false, true),
739 Command::ToggleTie(c) => hint!(meas!(c), false, true),
740 Command::SetDynamic(c) => hint!(meas!(c), false, true),
741 Command::ToggleArticulation(c) => hint!(meas!(c), false, true),
742 Command::SetGrace(c) => hint!(meas!(c), false, true),
743 Command::SetOttava(c) => hint!(meas!(c), false, true),
744 Command::SetLyric(c) => hint!(meas!(c), false, true),
745 Command::AddPedal(c) => hint!(meas!(c), false, true),
746 Command::SetChordSymbol(c) => hint!(meas!(c), false, true),
747
748 Command::SetSystemBreak(_) | Command::SetPageBreak(_)
749 => hint!(Global, true, false),
750
751 Command::ToggleSlur(_) | Command::ToggleTrillLine(_)
752 => hint!(Global, true, false),
753
754 Command::SetPartGroup(_)
755 => hint!(Global, false, false),
756
757 Command::AddStaff(_) | Command::DeleteStaff(_)
758 => hint!(Global, true, true),
759
760 Command::SetTuplet(c)
761 => hint!(meas!(c), false, true),
762
763 Command::RespellScore(_) | Command::RespellScoreToKey(_)
764 => hint!(Global, true, true),
765
766 Command::SetStem(c)
767 => hint!(meas!(c), false, false),
768
769 Command::SetArpeggio(c)
770 => hint!(meas!(c), false, false),
771
772 Command::SetTechniqueText(c) => hint!(meas!(c), false, false),
773 Command::SetFingering(c) => hint!(meas!(c), false, false),
774 Command::SetStringNumber(c) => hint!(meas!(c), false, false),
775 Command::SetGuitarTechnique(c) => hint!(meas!(c), false, false),
776 Command::SetNoteHead(c) => hint!(meas!(c), false, false),
777 Command::SetCue(c) => hint!(meas!(c), false, true),
778
779 Command::Batch(c) => {
780 let Some(first) = c.commands.first() else {
781 return hint!(Global, false, false);
782 };
783 let mut merged = command_hint(first);
784 for cmd in c.commands.iter().skip(1) {
785 merged = merged.merge(command_hint(cmd));
786 }
787 merged
788 }
789 }
790}
791
792pub fn command_label(cmd: &Command) -> String {
794 match cmd {
795 Command::AddNote(_) => "Add Note".to_string(),
796 Command::AddPitch(_) => "Add Pitch".to_string(),
797 Command::DeleteNote(_) => "Delete Note".to_string(),
798 Command::AddMeasure(_) => "Add Measure".to_string(),
799 Command::DeleteMeasure(_) => "Delete Measure".to_string(),
800 Command::SetTempo(_) => "Set Tempo".to_string(),
801 Command::NewScore(_) => "New Score".to_string(),
802 Command::AddHairpin(_) => "Add Hairpin".to_string(),
803 Command::ToggleTie(_) => "Toggle Tie".to_string(),
804 Command::SetDynamic(_) => "Set Dynamic".to_string(),
805 Command::ToggleArticulation(_) => "Toggle Articulation".to_string(),
806 Command::SetKeySignature(_) => "Set Key Signature".to_string(),
807 Command::SetTimeSignature(_) => "Set Time Signature".to_string(),
808 Command::SetBarline(_) => "Set Barline".to_string(),
809 Command::AddPart(_) => "Add Part".to_string(),
810 Command::DeletePart(_) => "Delete Part".to_string(),
811 Command::SetMetadata(_) => "Set Metadata".to_string(),
812 Command::SetRehearsalMark(_) => "Set Rehearsal Mark".to_string(),
813 Command::SetNavigationMark(_) => "Set Navigation Mark".to_string(),
814 Command::SetChordSymbol(_) => "Set Chord Symbol".to_string(),
815 Command::SetGrace(_) => "Set Grace Note".to_string(),
816 Command::SetOttava(_) => "Set Ottava".to_string(),
817 Command::SetLyric(_) => "Set Lyric".to_string(),
818 Command::SetMultiRest(_) => "Set Multi-Rest".to_string(),
819 Command::AddPedal(_) => "Add Pedal".to_string(),
820 Command::SetVolta(_) => "Set Volta".to_string(),
821 Command::SetClef(_) => "Set Clef".to_string(),
822 Command::SetPartName(_) => "Set Part Name".to_string(),
823 Command::SetMidiInstrument(_) => "Set MIDI Instrument".to_string(),
824 Command::SetTranspose(_) => "Set Transpose".to_string(),
825 Command::SetTempoAtMeasure(_) => "Set Tempo".to_string(),
826 Command::PasteVoice(_) => "Paste Voice".to_string(),
827 Command::PasteRange(_) => "Paste Range".to_string(),
828 Command::SetSystemBreak(_) => "Set System Break".to_string(),
829 Command::SetPageBreak(_) => "Set Page Break".to_string(),
830 Command::ToggleSlur(_) => "Toggle Slur".to_string(),
831 Command::AddStaff(_) => "Add Staff".to_string(),
832 Command::DeleteStaff(_) => "Delete Staff".to_string(),
833 Command::SetTuplet(c) => if c.tuplet.is_some() { "Set Tuplet" } else { "Clear Tuplet" }.to_string(),
834 Command::RespellScore(c) => if c.prefer_flat { "Respell Score (flat)" } else { "Respell Score (sharp)" }.to_string(),
835 Command::RespellScoreToKey(_) => "Respell Score to Key".to_string(),
836 Command::SetStem(_) => "Set Stem".to_string(),
837 Command::SetArpeggio(_) => "Set Arpeggio".to_string(),
838 Command::SetTechniqueText(_) => "Set Technique Text".to_string(),
839 Command::SetFingering(_) => "Set Fingering".to_string(),
840 Command::SetStringNumber(_) => "Set String Number".to_string(),
841 Command::SetGuitarTechnique(_) => "Set Guitar Technique".to_string(),
842 Command::SetNoteHead(_) => "Set Note Head".to_string(),
843 Command::SetCue(c) => if c.is_cue { "Set Cue Note" } else { "Clear Cue Note" }.to_string(),
844 Command::SetExpressionText(_) => "Set Expression Text".to_string(),
845 Command::ToggleTrillLine(_) => "Toggle Trill Line".to_string(),
846 Command::SetPartGroup(_) => "Set Part Group".to_string(),
847 Command::Batch(c) => c.label.clone()
848 .unwrap_or_else(|| c.commands.first()
849 .map(command_label)
850 .unwrap_or_else(|| "Batch".to_string())),
851 }
852}
853
854pub fn command_key(cmd: &Command) -> String {
858 match cmd {
859 Command::AddNote(_) => "AddNote".to_string(),
860 Command::AddPitch(_) => "AddPitch".to_string(),
861 Command::DeleteNote(_) => "DeleteNote".to_string(),
862 Command::AddMeasure(_) => "AddMeasure".to_string(),
863 Command::DeleteMeasure(_) => "DeleteMeasure".to_string(),
864 Command::SetTempo(_) => "SetTempo".to_string(),
865 Command::NewScore(_) => "NewScore".to_string(),
866 Command::AddHairpin(_) => "AddHairpin".to_string(),
867 Command::ToggleTie(_) => "ToggleTie".to_string(),
868 Command::SetDynamic(_) => "SetDynamic".to_string(),
869 Command::ToggleArticulation(_) => "ToggleArticulation".to_string(),
870 Command::SetKeySignature(_) => "SetKeySignature".to_string(),
871 Command::SetTimeSignature(_) => "SetTimeSignature".to_string(),
872 Command::SetBarline(_) => "SetBarline".to_string(),
873 Command::AddPart(_) => "AddPart".to_string(),
874 Command::DeletePart(_) => "DeletePart".to_string(),
875 Command::SetMetadata(_) => "SetMetadata".to_string(),
876 Command::SetRehearsalMark(_) => "SetRehearsalMark".to_string(),
877 Command::SetNavigationMark(_) => "SetNavigationMark".to_string(),
878 Command::SetChordSymbol(_) => "SetChordSymbol".to_string(),
879 Command::SetGrace(_) => "SetGrace".to_string(),
880 Command::SetOttava(_) => "SetOttava".to_string(),
881 Command::SetLyric(_) => "SetLyric".to_string(),
882 Command::SetMultiRest(_) => "SetMultiRest".to_string(),
883 Command::AddPedal(_) => "AddPedal".to_string(),
884 Command::SetVolta(_) => "SetVolta".to_string(),
885 Command::SetClef(_) => "SetClef".to_string(),
886 Command::SetPartName(_) => "SetPartName".to_string(),
887 Command::SetMidiInstrument(_) => "SetMidiInstrument".to_string(),
888 Command::SetTranspose(_) => "SetTranspose".to_string(),
889 Command::SetTempoAtMeasure(_) => "SetTempoAtMeasure".to_string(),
890 Command::PasteVoice(_) => "PasteVoice".to_string(),
891 Command::PasteRange(_) => "PasteRange".to_string(),
892 Command::SetSystemBreak(_) => "SetSystemBreak".to_string(),
893 Command::SetPageBreak(_) => "SetPageBreak".to_string(),
894 Command::ToggleSlur(_) => "ToggleSlur".to_string(),
895 Command::AddStaff(_) => "AddStaff".to_string(),
896 Command::DeleteStaff(_) => "DeleteStaff".to_string(),
897 Command::SetTuplet(_) => "SetTuplet".to_string(),
898 Command::RespellScore(_) => "RespellScore".to_string(),
899 Command::RespellScoreToKey(_) => "RespellScoreToKey".to_string(),
900 Command::SetStem(_) => "SetStem".to_string(),
901 Command::SetArpeggio(_) => "SetArpeggio".to_string(),
902 Command::SetTechniqueText(_) => "SetTechniqueText".to_string(),
903 Command::SetFingering(_) => "SetFingering".to_string(),
904 Command::SetStringNumber(_) => "SetStringNumber".to_string(),
905 Command::SetGuitarTechnique(_) => "SetGuitarTechnique".to_string(),
906 Command::SetNoteHead(_) => "SetNoteHead".to_string(),
907 Command::SetCue(_) => "SetCue".to_string(),
908 Command::SetExpressionText(_) => "SetExpressionText".to_string(),
909 Command::ToggleTrillLine(_) => "ToggleTrillLine".to_string(),
910 Command::SetPartGroup(_) => "SetPartGroup".to_string(),
911 Command::Batch(c) => c.label.clone().unwrap_or_else(|| "Batch".to_string()),
912 }
913}
914
915pub fn apply_command(cmd: &Command, score: &mut Score) -> Result<(), Error> {
916 match cmd {
917 Command::AddNote(c) => apply_add_note(c, score),
918 Command::AddPitch(c) => apply_add_pitch(c, score),
919 Command::DeleteNote(c) => apply_delete_note(c, score),
920 Command::AddMeasure(c) => apply_add_measure(c, score),
921 Command::DeleteMeasure(c) => apply_delete_measure(c, score),
922 Command::SetTempo(c) => { score.settings.tempo_bpm = c.bpm; Ok(()) }
923 Command::NewScore(c) => {
924 let mut s = match c.template {
925 Some(kind) => Score::template(kind),
926 None => Score::new(
927 &c.title, c.tempo_bpm,
928 c.time_numerator, c.time_denominator,
929 c.key_fifths, c.measure_count,
930 ),
931 };
932 if c.template.is_some() {
933 s.metadata.title = c.title.clone();
934 s.metadata.composer = c.composer.clone();
935 s.settings.tempo_bpm = c.tempo_bpm;
936 s.settings.time_signature = TimeSignature {
937 numerator: c.time_numerator, denominator: c.time_denominator,
938 };
939 s.settings.key_signature = KeySignature {
940 fifths: c.key_fifths, mode: "major".to_string(),
941 };
942 for part in &mut s.parts {
943 for staff in &mut part.staves {
944 staff.measures.clear();
945 for i in 0..c.measure_count {
946 let mut m = Measure::empty(c.time_numerator, c.time_denominator);
947 m.number = i + 1;
948 staff.measures.push(m);
949 }
950 }
951 }
952 }
953 *score = s;
954 Ok(())
955 }
956 Command::AddHairpin(c) => apply_add_hairpin(c, score),
957 Command::ToggleTie(c) => apply_toggle_tie(c, score),
958 Command::SetDynamic(c) => apply_set_dynamic(c, score),
959 Command::ToggleArticulation(c) => apply_toggle_articulation(c, score),
960 Command::SetKeySignature(c) => {
961 score.settings.key_signature = KeySignature { fifths: c.fifths, mode: "major".to_string() };
962 Ok(())
963 }
964 Command::SetTimeSignature(c) => apply_set_time_signature(c, score),
965 Command::SetBarline(c) => apply_set_barline(c, score),
966 Command::AddPart(c) => apply_add_part(c, score),
967 Command::DeletePart(c) => apply_delete_part(c, score),
968 Command::SetMetadata(c) => apply_set_metadata(c, score),
969 Command::SetRehearsalMark(c) => {
970 for_each_measure_at(score, c.measure_index, |m| { m.rehearsal = c.text.clone(); });
971 Ok(())
972 }
973 Command::SetNavigationMark(c) => {
974 for_each_measure_at(score, c.measure_index, |m| { m.navigation = c.mark.clone(); });
975 Ok(())
976 }
977 Command::SetChordSymbol(c) => {
978 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice, c.note_index)?
979 .chord_symbol = c.chord.clone();
980 Ok(())
981 }
982 Command::SetGrace(c) => {
983 let note = get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice, c.note_index)?;
984 if note.is_rest {
985 return Err(Error::InvalidCommand("cannot make a rest into a grace note".into()));
986 }
987 note.is_grace = c.is_grace;
988 note.grace_slash = c.slash;
989 Ok(())
990 }
991 Command::SetOttava(c) => {
992 let note = get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice, c.note_index)?;
993 note.ottava_start = c.ottava_start;
994 note.ottava_end = c.ottava_end;
995 Ok(())
996 }
997 Command::SetLyric(c) => {
998 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice, c.note_index)?
999 .lyric = c.lyric.clone();
1000 Ok(())
1001 }
1002 Command::SetMultiRest(c) => {
1003 for_each_measure_at(score, c.measure_index, |m| { m.multi_rest_count = c.count; });
1004 Ok(())
1005 }
1006 Command::AddPedal(c) => apply_add_pedal(c, score),
1007 Command::SetVolta(c) => {
1008 for_each_measure_at(score, c.measure_index, |m| { m.volta = c.volta.clone(); });
1009 Ok(())
1010 }
1011 Command::SetClef(c) => apply_set_clef(c, score),
1012 Command::SetPartName(c) => apply_set_part_name(c, score),
1013 Command::SetMidiInstrument(c) => apply_set_midi_instrument(c, score),
1014 Command::SetTranspose(c) => apply_set_transpose(c, score),
1015 Command::SetTempoAtMeasure(c) => {
1016 for_each_measure_at(score, c.measure_index, |m| { m.tempo = c.bpm; });
1017 Ok(())
1018 }
1019 Command::PasteVoice(c) => apply_paste_voice(c, score),
1020 Command::PasteRange(c) => apply_paste_range(c, score),
1021 Command::SetSystemBreak(c) => {
1022 for_each_measure_at(score, c.measure_index, |m| { m.system_break = c.value; });
1023 Ok(())
1024 }
1025 Command::SetPageBreak(c) => {
1026 for_each_measure_at(score, c.measure_index, |m| { m.page_break = c.value; });
1027 Ok(())
1028 }
1029 Command::ToggleSlur(c) => apply_toggle_slur(c, score),
1030 Command::AddStaff(c) => apply_add_staff(c, score),
1031 Command::DeleteStaff(c) => apply_delete_staff(c, score),
1032 Command::SetTuplet(c) => {
1033 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice_index, c.note_index)?
1034 .tuplet = c.tuplet.clone();
1035 Ok(())
1036 }
1037 Command::RespellScore(c) => { respell_score(score, c.prefer_flat); Ok(()) }
1038 Command::RespellScoreToKey(_) => { respell_score_to_key(score); Ok(()) }
1039 Command::SetStem(c) => {
1040 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice_index, c.note_index)?
1041 .stem_up = c.stem_up;
1042 Ok(())
1043 }
1044 Command::SetArpeggio(c) => {
1045 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice_index, c.note_index)?
1046 .arpeggiate = c.direction;
1047 Ok(())
1048 }
1049 Command::SetTechniqueText(c) => {
1050 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice, c.note_index)?
1051 .technique_text = c.text.clone();
1052 Ok(())
1053 }
1054 Command::SetFingering(c) => {
1055 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice, c.note_index)?
1056 .fingering = c.fingering;
1057 Ok(())
1058 }
1059 Command::SetStringNumber(c) => {
1060 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice, c.note_index)?
1061 .string_number = c.string_number;
1062 Ok(())
1063 }
1064 Command::SetGuitarTechnique(c) => {
1065 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice, c.note_index)?
1066 .guitar_technique = c.technique.clone();
1067 Ok(())
1068 }
1069 Command::SetNoteHead(c) => {
1070 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice, c.note_index)?
1071 .note_head = c.note_head.clone();
1072 Ok(())
1073 }
1074 Command::SetCue(c) => {
1075 get_note_mut(score, c.part_index, c.staff_index, c.measure_index, c.voice, c.note_index)?
1076 .is_cue = c.is_cue;
1077 Ok(())
1078 }
1079 Command::SetExpressionText(c) => {
1080 for_each_measure_at(score, c.measure_index, |m| { m.expression_text = c.text.clone(); });
1081 Ok(())
1082 }
1083 Command::ToggleTrillLine(c) => apply_toggle_trill_line(c, score),
1084 Command::SetPartGroup(c) => {
1085 if let Some(group) = &c.group {
1086 score.part_groups.retain(|g| g.first_part != group.first_part || g.last_part != group.last_part);
1087 score.part_groups.push(group.clone());
1088 } else {
1089 score.part_groups.clear();
1091 }
1092 Ok(())
1093 }
1094 Command::Batch(c) => {
1095 for cmd in &c.commands { apply_command(cmd, score)?; }
1096 Ok(())
1097 }
1098 }
1099}
1100
1101fn get_note_mut(
1104 score: &mut Score,
1105 part_index: usize,
1106 staff_index: usize,
1107 measure_index: usize,
1108 voice: usize,
1109 note_index: usize,
1110) -> Result<&mut Note, Error> {
1111 score.parts
1112 .get_mut(part_index).ok_or(Error::PartNotFound(part_index))?
1113 .staves.get_mut(staff_index).ok_or(Error::StaffNotFound(staff_index))?
1114 .measures.get_mut(measure_index).ok_or(Error::MeasureNotFound(measure_index))?
1115 .voices.get_mut(voice).ok_or(Error::VoiceOutOfRange(voice))?
1116 .get_mut(note_index).ok_or(Error::NoteNotFound(note_index))
1117}
1118
1119fn for_each_measure_at(score: &mut Score, index: usize, mut f: impl FnMut(&mut Measure)) {
1120 for part in &mut score.parts {
1121 for staff in &mut part.staves {
1122 if let Some(m) = staff.measures.get_mut(index) {
1123 f(m);
1124 }
1125 }
1126 }
1127}
1128
1129fn apply_add_note(cmd: &AddNoteCmd, score: &mut Score) -> Result<(), Error> {
1130 let ts_beats = score.settings.time_signature.total_beats();
1131 let voice = score.parts
1132 .get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?
1133 .staves.get_mut(cmd.staff_index).ok_or(Error::StaffNotFound(cmd.staff_index))?
1134 .measures.get_mut(cmd.measure_index).ok_or(Error::MeasureNotFound(cmd.measure_index))?
1135 .voices.get_mut(cmd.voice).ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1136
1137 let note = if cmd.is_rest {
1138 let mut n = Note::rest(cmd.duration.clone());
1139 n.dot_count = cmd.dot_count;
1140 n.tuplet = cmd.tuplet.clone();
1141 n
1142 } else {
1143 let pitch = cmd.pitch.clone()
1144 .ok_or_else(|| Error::InvalidCommand("pitch required for non-rest note".into()))?;
1145 let mut n = Note::new(pitch, cmd.duration.clone());
1146 n.dot_count = cmd.dot_count;
1147 n.tuplet = cmd.tuplet.clone();
1148 n
1149 };
1150
1151 let pos = cmd.position.min(voice.len());
1152 voice.insert(pos, note);
1153 trim_voice_to_measure(voice, ts_beats);
1154 Ok(())
1155}
1156
1157fn apply_add_pitch(cmd: &AddPitchCmd, score: &mut Score) -> Result<(), Error> {
1158 let voice = score.parts
1159 .get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?
1160 .staves.get_mut(cmd.staff_index).ok_or(Error::StaffNotFound(cmd.staff_index))?
1161 .measures.get_mut(cmd.measure_index).ok_or(Error::MeasureNotFound(cmd.measure_index))?
1162 .voices.get_mut(cmd.voice).ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1163 let note = voice.get_mut(cmd.note_index).ok_or(Error::NoteNotFound(cmd.note_index))?;
1164 if note.is_rest {
1165 return Err(Error::InvalidCommand("cannot add pitch to a rest".into()));
1166 }
1167 if !note.pitches.iter().any(|p| p.step == cmd.pitch.step && p.octave == cmd.pitch.octave) {
1168 note.pitches.push(cmd.pitch.clone());
1169 }
1170 Ok(())
1171}
1172
1173fn apply_delete_note(cmd: &DeleteNoteCmd, score: &mut Score) -> Result<(), Error> {
1174 let ts_beats = score.settings.time_signature.total_beats();
1175 let voice = score.parts
1176 .get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?
1177 .staves.get_mut(cmd.staff_index).ok_or(Error::StaffNotFound(cmd.staff_index))?
1178 .measures.get_mut(cmd.measure_index).ok_or(Error::MeasureNotFound(cmd.measure_index))?
1179 .voices.get_mut(cmd.voice).ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1180 voice.retain(|n| n.id != cmd.note_id);
1181 pad_voice_to_measure(voice, ts_beats);
1182 Ok(())
1183}
1184
1185fn apply_add_measure(cmd: &AddMeasureCmd, score: &mut Score) -> Result<(), Error> {
1186 let ts = score.settings.time_signature.clone();
1187 for part in &mut score.parts {
1188 for staff in &mut part.staves {
1189 let insert_at = (cmd.after_index + 1).min(staff.measures.len());
1190 let mut m = Measure::empty(ts.numerator, ts.denominator);
1191 m.number = insert_at as u32 + 1;
1192 staff.measures.insert(insert_at, m);
1193 for (i, measure) in staff.measures.iter_mut().enumerate() {
1194 measure.number = i as u32 + 1;
1195 }
1196 }
1197 }
1198 Ok(())
1199}
1200
1201fn apply_delete_measure(cmd: &DeleteMeasureCmd, score: &mut Score) -> Result<(), Error> {
1202 for part in &mut score.parts {
1203 for staff in &mut part.staves {
1204 if cmd.measure_index < staff.measures.len() {
1205 staff.measures.remove(cmd.measure_index);
1206 for (i, m) in staff.measures.iter_mut().enumerate() {
1207 m.number = i as u32 + 1;
1208 }
1209 }
1210 }
1211 }
1212 Ok(())
1213}
1214
1215fn apply_add_hairpin(cmd: &AddHairpinCmd, score: &mut Score) -> Result<(), Error> {
1216 let voice = score.parts
1217 .get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?
1218 .staves.get_mut(cmd.staff_index).ok_or(Error::StaffNotFound(cmd.staff_index))?
1219 .measures.get_mut(cmd.measure_index).ok_or(Error::MeasureNotFound(cmd.measure_index))?
1220 .voices.get_mut(cmd.voice).ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1221 if cmd.start_note_idx >= voice.len() {
1222 return Err(Error::NoteNotFound(cmd.start_note_idx));
1223 }
1224 if cmd.end_note_idx >= voice.len() {
1225 return Err(Error::NoteNotFound(cmd.end_note_idx));
1226 }
1227 if cmd.start_note_idx >= cmd.end_note_idx {
1228 return Err(Error::InvalidCommand("start_note_idx must be less than end_note_idx".into()));
1229 }
1230 for note in voice.iter_mut().take(cmd.end_note_idx + 1).skip(cmd.start_note_idx) {
1231 note.hairpin_start = None;
1232 note.hairpin_end = false;
1233 }
1234 voice[cmd.start_note_idx].hairpin_start = Some(cmd.kind);
1235 voice[cmd.end_note_idx].hairpin_end = true;
1236 Ok(())
1237}
1238
1239fn apply_add_pedal(cmd: &AddPedalCmd, score: &mut Score) -> Result<(), Error> {
1240 let voice = score.parts
1241 .get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?
1242 .staves.get_mut(cmd.staff_index).ok_or(Error::StaffNotFound(cmd.staff_index))?
1243 .measures.get_mut(cmd.measure_index).ok_or(Error::MeasureNotFound(cmd.measure_index))?
1244 .voices.get_mut(cmd.voice).ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1245 if cmd.start_note_idx >= voice.len() {
1246 return Err(Error::NoteNotFound(cmd.start_note_idx));
1247 }
1248 if cmd.end_note_idx >= voice.len() {
1249 return Err(Error::NoteNotFound(cmd.end_note_idx));
1250 }
1251 if cmd.start_note_idx >= cmd.end_note_idx {
1252 return Err(Error::InvalidCommand("start_note_idx must be less than end_note_idx".into()));
1253 }
1254 for note in voice.iter_mut().take(cmd.end_note_idx + 1).skip(cmd.start_note_idx) {
1255 note.pedal_start = false;
1256 note.pedal_end = false;
1257 }
1258 voice[cmd.start_note_idx].pedal_start = true;
1259 voice[cmd.end_note_idx].pedal_end = true;
1260 Ok(())
1261}
1262
1263fn apply_toggle_tie(cmd: &ToggleTieCmd, score: &mut Score) -> Result<(), Error> {
1264 let current_tie_start = {
1265 let v = score.parts
1266 .get(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?
1267 .staves.get(cmd.staff_index).ok_or(Error::StaffNotFound(cmd.staff_index))?
1268 .measures.get(cmd.measure_index).ok_or(Error::MeasureNotFound(cmd.measure_index))?
1269 .voices.get(cmd.voice).ok_or(Error::VoiceOutOfRange(cmd.voice))?;
1270 v.get(cmd.note_index).ok_or(Error::NoteNotFound(cmd.note_index))?.tie_start
1271 };
1272 let voice_len = score.parts[cmd.part_index].staves[cmd.staff_index]
1273 .measures[cmd.measure_index].voices[cmd.voice].len();
1274 let total_measures = score.parts[cmd.part_index].staves[cmd.staff_index].measures.len();
1275
1276 let new_tie = !current_tie_start;
1277 score.parts[cmd.part_index].staves[cmd.staff_index]
1278 .measures[cmd.measure_index].voices[cmd.voice][cmd.note_index].tie_start = new_tie;
1279
1280 if cmd.note_index + 1 < voice_len {
1281 score.parts[cmd.part_index].staves[cmd.staff_index]
1282 .measures[cmd.measure_index].voices[cmd.voice][cmd.note_index + 1].tie_end = new_tie;
1283 } else {
1284 let next_mi = cmd.measure_index + 1;
1285 if next_mi < total_measures {
1286 let next_voice = &mut score.parts[cmd.part_index].staves[cmd.staff_index]
1287 .measures[next_mi].voices[cmd.voice];
1288 if let Some(n) = next_voice.get_mut(0) {
1289 n.tie_end = new_tie;
1290 }
1291 }
1292 }
1293 Ok(())
1294}
1295
1296fn apply_set_dynamic(cmd: &SetDynamicCmd, score: &mut Score) -> Result<(), Error> {
1297 get_note_mut(score, cmd.part_index, cmd.staff_index, cmd.measure_index, cmd.voice, cmd.note_index)?
1298 .dynamic = cmd.dynamic.clone();
1299 Ok(())
1300}
1301
1302fn apply_toggle_articulation(cmd: &ToggleArticulationCmd, score: &mut Score) -> Result<(), Error> {
1303 let note = get_note_mut(score, cmd.part_index, cmd.staff_index, cmd.measure_index, cmd.voice, cmd.note_index)?;
1304 if let Some(pos) = note.articulations.iter().position(|a| a == &cmd.articulation) {
1305 note.articulations.remove(pos);
1306 } else {
1307 note.articulations.push(cmd.articulation.clone());
1308 }
1309 Ok(())
1310}
1311
1312fn apply_set_time_signature(cmd: &SetTimeSignatureCmd, score: &mut Score) -> Result<(), Error> {
1313 if cmd.numerator == 0 || cmd.denominator == 0 {
1314 return Err(Error::InvalidCommand("time signature numerator and denominator must be > 0".into()));
1315 }
1316 if ![1u8, 2, 4, 8, 16, 32].contains(&cmd.denominator) {
1317 return Err(Error::InvalidCommand(format!("invalid time signature denominator: {}", cmd.denominator)));
1318 }
1319 score.settings.time_signature = TimeSignature { numerator: cmd.numerator, denominator: cmd.denominator };
1320 let max_beats = score.settings.time_signature.total_beats();
1321 for part in &mut score.parts {
1322 for staff in &mut part.staves {
1323 for measure in &mut staff.measures {
1324 for voice in &mut measure.voices {
1325 trim_voice_to_measure(voice, max_beats);
1326 pad_voice_to_measure(voice, max_beats);
1327 }
1328 }
1329 }
1330 }
1331 Ok(())
1332}
1333
1334fn apply_set_barline(cmd: &SetBarlineCmd, score: &mut Score) -> Result<(), Error> {
1335 for part in &mut score.parts {
1336 for staff in &mut part.staves {
1337 let measure = staff.measures.get_mut(cmd.measure_index)
1338 .ok_or(Error::MeasureNotFound(cmd.measure_index))?;
1339 match cmd.side.as_str() {
1340 "left" => measure.barline_left = cmd.barline.clone(),
1341 "right" => measure.barline_right = cmd.barline.clone(),
1342 _ => return Err(Error::InvalidCommand(format!("invalid barline side: '{}'", cmd.side))),
1343 }
1344 }
1345 }
1346 Ok(())
1347}
1348
1349fn apply_add_part(cmd: &AddPartCmd, score: &mut Score) -> Result<(), Error> {
1350 if cmd.clefs.is_empty() {
1351 return Err(Error::InvalidCommand("AddPart requires at least one clef".into()));
1352 }
1353 let measure_count = score.measure_count();
1354 let ts = score.settings.time_signature.clone();
1355 let mut part = Part::new(&cmd.name, &cmd.short_name);
1356 part.midi_channel = cmd.midi_channel.min(15);
1357 part.midi_program = cmd.midi_program;
1358 for clef_str in &cmd.clefs {
1359 let clef = match clef_str.as_str() {
1360 "Bass" => Clef::Bass,
1361 "Alto" => Clef::Alto,
1362 "Tenor" => Clef::Tenor,
1363 "Percussion" => Clef::Percussion,
1364 _ => Clef::Treble,
1365 };
1366 let mut staff = Staff::new(clef);
1367 for i in 0..measure_count {
1368 let mut m = Measure::empty(ts.numerator, ts.denominator);
1369 m.number = i as u32 + 1;
1370 staff.measures.push(m);
1371 }
1372 part.staves.push(staff);
1373 }
1374 score.parts.push(part);
1375 Ok(())
1376}
1377
1378fn apply_set_clef(cmd: &SetClefCmd, score: &mut Score) -> Result<(), Error> {
1379 score.parts
1380 .get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?
1381 .staves.get_mut(cmd.staff_index).ok_or(Error::StaffNotFound(cmd.staff_index))?
1382 .clef = cmd.clef.clone();
1383 Ok(())
1384}
1385
1386fn apply_set_part_name(cmd: &SetPartNameCmd, score: &mut Score) -> Result<(), Error> {
1387 let part = score.parts.get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?;
1388 part.name = cmd.name.clone();
1389 part.short_name = cmd.short_name.clone();
1390 Ok(())
1391}
1392
1393fn apply_delete_part(cmd: &DeletePartCmd, score: &mut Score) -> Result<(), Error> {
1394 if cmd.part_index >= score.parts.len() {
1395 return Err(Error::PartNotFound(cmd.part_index));
1396 }
1397 score.parts.remove(cmd.part_index);
1398 Ok(())
1399}
1400
1401fn apply_set_metadata(cmd: &SetMetadataCmd, score: &mut Score) -> Result<(), Error> {
1402 if let Some(v) = &cmd.title { score.metadata.title = v.clone(); }
1403 if let Some(v) = &cmd.composer { score.metadata.composer = v.clone(); }
1404 if let Some(v) = &cmd.lyricist { score.metadata.lyricist = v.clone(); }
1405 if let Some(v) = &cmd.copyright { score.metadata.copyright = v.clone(); }
1406 if let Some(v) = &cmd.work_number { score.metadata.work_number = v.clone(); }
1407 if let Some(v) = &cmd.movement_title { score.metadata.movement_title = v.clone(); }
1408 Ok(())
1409}
1410
1411fn apply_set_midi_instrument(cmd: &SetMidiInstrumentCmd, score: &mut Score) -> Result<(), Error> {
1412 let part = score.parts.get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?;
1413 part.midi_channel = cmd.midi_channel.min(15);
1414 part.midi_program = cmd.midi_program;
1415 Ok(())
1416}
1417
1418fn apply_set_transpose(cmd: &SetTransposeCmd, score: &mut Score) -> Result<(), Error> {
1419 score.parts
1420 .get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?
1421 .staves.get_mut(cmd.staff_index).ok_or(Error::StaffNotFound(cmd.staff_index))?
1422 .transpose_semitones = cmd.semitones;
1423 Ok(())
1424}
1425
1426fn apply_paste_voice(cmd: &PasteVoiceCmd, score: &mut Score) -> Result<(), Error> {
1427 let voice = score.parts
1428 .get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?
1429 .staves.get_mut(cmd.staff_index).ok_or(Error::StaffNotFound(cmd.staff_index))?
1430 .measures.get_mut(cmd.measure_index).ok_or(Error::MeasureNotFound(cmd.measure_index))?
1431 .voices.get_mut(cmd.voice_index).ok_or(Error::VoiceOutOfRange(cmd.voice_index))?;
1432 *voice = cmd.notes.clone();
1433 Ok(())
1434}
1435
1436fn apply_paste_range(cmd: &PasteRangeCmd, score: &mut Score) -> Result<(), Error> {
1437 let part = score.parts.get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?;
1438 let staff = part.staves.get_mut(cmd.staff_index).ok_or(Error::StaffNotFound(cmd.staff_index))?;
1439 if cmd.voice_index >= 4 { return Err(Error::VoiceOutOfRange(cmd.voice_index)); }
1440 for (offset, notes) in cmd.measures.iter().enumerate() {
1441 let mi = cmd.target_measure + offset;
1442 let measure = staff.measures.get_mut(mi).ok_or(Error::MeasureNotFound(mi))?;
1443 measure.voices[cmd.voice_index] = notes.clone();
1444 }
1445 Ok(())
1446}
1447
1448fn apply_toggle_slur(cmd: &ToggleSlurCmd, score: &mut Score) -> Result<(), Error> {
1449 let new_start = !{
1450 score.parts
1451 .get(cmd.start.part).ok_or(Error::PartNotFound(cmd.start.part))?
1452 .staves.get(cmd.start.staff).ok_or(Error::StaffNotFound(cmd.start.staff))?
1453 .measures.get(cmd.start.measure).ok_or(Error::MeasureNotFound(cmd.start.measure))?
1454 .voices.get(cmd.start.voice).ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
1455 .get(cmd.start.note).ok_or(Error::NoteNotFound(cmd.start.note))?
1456 .slur_start
1457 };
1458 let new_end = !{
1459 score.parts
1460 .get(cmd.end.part).ok_or(Error::PartNotFound(cmd.end.part))?
1461 .staves.get(cmd.end.staff).ok_or(Error::StaffNotFound(cmd.end.staff))?
1462 .measures.get(cmd.end.measure).ok_or(Error::MeasureNotFound(cmd.end.measure))?
1463 .voices.get(cmd.end.voice).ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
1464 .get(cmd.end.note).ok_or(Error::NoteNotFound(cmd.end.note))?
1465 .slur_end
1466 };
1467 score.parts[cmd.start.part].staves[cmd.start.staff]
1468 .measures[cmd.start.measure].voices[cmd.start.voice][cmd.start.note]
1469 .slur_start = new_start;
1470 score.parts[cmd.end.part].staves[cmd.end.staff]
1471 .measures[cmd.end.measure].voices[cmd.end.voice][cmd.end.note]
1472 .slur_end = new_end;
1473 Ok(())
1474}
1475
1476fn apply_toggle_trill_line(cmd: &ToggleTrillLineCmd, score: &mut Score) -> Result<(), Error> {
1477 let new_start = !{
1478 score.parts
1479 .get(cmd.start.part).ok_or(Error::PartNotFound(cmd.start.part))?
1480 .staves.get(cmd.start.staff).ok_or(Error::StaffNotFound(cmd.start.staff))?
1481 .measures.get(cmd.start.measure).ok_or(Error::MeasureNotFound(cmd.start.measure))?
1482 .voices.get(cmd.start.voice).ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
1483 .get(cmd.start.note).ok_or(Error::NoteNotFound(cmd.start.note))?
1484 .trill_line_start
1485 };
1486 let new_end = !{
1487 score.parts
1488 .get(cmd.end.part).ok_or(Error::PartNotFound(cmd.end.part))?
1489 .staves.get(cmd.end.staff).ok_or(Error::StaffNotFound(cmd.end.staff))?
1490 .measures.get(cmd.end.measure).ok_or(Error::MeasureNotFound(cmd.end.measure))?
1491 .voices.get(cmd.end.voice).ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
1492 .get(cmd.end.note).ok_or(Error::NoteNotFound(cmd.end.note))?
1493 .trill_line_end
1494 };
1495 score.parts[cmd.start.part].staves[cmd.start.staff]
1496 .measures[cmd.start.measure].voices[cmd.start.voice][cmd.start.note]
1497 .trill_line_start = new_start;
1498 score.parts[cmd.end.part].staves[cmd.end.staff]
1499 .measures[cmd.end.measure].voices[cmd.end.voice][cmd.end.note]
1500 .trill_line_end = new_end;
1501 Ok(())
1502}
1503
1504fn apply_add_staff(cmd: &AddStaffCmd, score: &mut Score) -> Result<(), Error> {
1505 let ts = score.settings.time_signature.clone();
1506 let measure_count = score.parts
1507 .get(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?
1508 .staves.first().map_or(0, |s| s.measures.len());
1509 let mut staff = Staff::new(cmd.clef.clone());
1510 for i in 0..measure_count {
1511 let mut m = Measure::empty(ts.numerator, ts.denominator);
1512 m.number = i as u32 + 1;
1513 staff.measures.push(m);
1514 }
1515 score.parts[cmd.part_index].staves.push(staff);
1516 Ok(())
1517}
1518
1519fn apply_delete_staff(cmd: &DeleteStaffCmd, score: &mut Score) -> Result<(), Error> {
1520 let part = score.parts.get_mut(cmd.part_index).ok_or(Error::PartNotFound(cmd.part_index))?;
1521 if part.staves.len() <= 1 {
1522 return Err(Error::CannotDeleteLastStaff);
1523 }
1524 if cmd.staff_index >= part.staves.len() {
1525 return Err(Error::StaffNotFound(cmd.staff_index));
1526 }
1527 part.staves.remove(cmd.staff_index);
1528 Ok(())
1529}
1530
1531fn trim_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
1532 let mut total = 0.0f64;
1533 let mut cutoff = voice.len();
1534 for (i, n) in voice.iter().enumerate() {
1535 total += n.beats();
1536 if total > max_beats + 1e-9 {
1537 cutoff = i;
1538 break;
1539 }
1540 }
1541 voice.truncate(cutoff);
1542 pad_voice_to_measure(voice, max_beats);
1543}
1544
1545fn pad_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
1546 let mut used: f64 = voice.iter().map(|n| n.beats()).sum();
1547 while max_beats - used > 1e-9 {
1548 let remaining = max_beats - used;
1549 let rest = Note::rest(Duration::whole_filling_beats(remaining));
1550 used += rest.beats();
1551 voice.push(rest);
1552 }
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557 use super::*;
1558 use crate::model::pitch::Step;
1559
1560 fn default_engine_score() -> Score {
1561 let mut s = Score::default();
1562 for part in &mut s.parts {
1563 for staff in &mut part.staves {
1564 for (i, m) in staff.measures.iter_mut().enumerate() {
1565 m.number = i as u32 + 1;
1566 }
1567 }
1568 }
1569 s
1570 }
1571
1572 #[test]
1573 fn add_note_inserts_into_voice() {
1574 let mut score = default_engine_score();
1575 let cmd = Command::AddNote(AddNoteCmd {
1576 part_index: 0, staff_index: 0, measure_index: 0, voice: 0,
1577 position: 0,
1578 pitch: Some(Pitch::new(Step::C, 4)),
1579 duration: Duration::Quarter,
1580 dot_count: 0,
1581 is_rest: false,
1582 tuplet: None,
1583 });
1584 apply_command(&cmd, &mut score).unwrap();
1585 let first = &score.parts[0].staves[0].measures[0].voices[0][0];
1586 assert!(!first.is_rest);
1587 assert_eq!(first.pitches[0].step, Step::C);
1588 }
1589
1590 #[test]
1591 fn set_tempo_updates_score() {
1592 let mut score = default_engine_score();
1593 let cmd = Command::SetTempo(SetTempoCmd { bpm: 160 });
1594 apply_command(&cmd, &mut score).unwrap();
1595 assert_eq!(score.settings.tempo_bpm, 160);
1596 }
1597
1598 #[test]
1599 fn add_measure_increases_count() {
1600 let mut score = default_engine_score();
1601 let before = score.measure_count();
1602 apply_command(&Command::AddMeasure(AddMeasureCmd { after_index: 0 }), &mut score).unwrap();
1603 assert_eq!(score.measure_count(), before + 1);
1604 }
1605
1606 #[test]
1607 fn delete_measure_decreases_count() {
1608 let mut score = default_engine_score();
1609 let before = score.measure_count();
1610 apply_command(&Command::DeleteMeasure(DeleteMeasureCmd { measure_index: 0 }), &mut score).unwrap();
1611 assert_eq!(score.measure_count(), before - 1);
1612 }
1613
1614 #[test]
1615 fn undo_restores_score() {
1616 let mut stack = CommandStack::new(50);
1617 let mut score = default_engine_score();
1618 let before = score.settings.tempo_bpm;
1619 stack.execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score).unwrap();
1620 assert_eq!(score.settings.tempo_bpm, 200);
1621 stack.undo(&mut score).unwrap();
1622 assert_eq!(score.settings.tempo_bpm, before);
1623 }
1624
1625 #[test]
1626 fn redo_reapplies_command() {
1627 let mut stack = CommandStack::new(50);
1628 let mut score = default_engine_score();
1629 stack.execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score).unwrap();
1630 stack.undo(&mut score).unwrap();
1631 stack.redo(&mut score).unwrap();
1632 assert_eq!(score.settings.tempo_bpm, 200);
1633 }
1634
1635 #[test]
1636 fn undo_nothing_returns_error() {
1637 let mut stack = CommandStack::new(50);
1638 let mut score = default_engine_score();
1639 assert!(stack.undo(&mut score).is_err());
1640 }
1641
1642 #[test]
1643 fn add_part_appends_part() {
1644 let mut score = default_engine_score();
1645 let before = score.parts.len();
1646 apply_command(&Command::AddPart(AddPartCmd {
1647 name: "Violin".into(),
1648 short_name: "Vln.".into(),
1649 clefs: vec!["Treble".into()],
1650 midi_channel: 0, midi_program: 0,
1651 }), &mut score).unwrap();
1652 assert_eq!(score.parts.len(), before + 1);
1653 }
1654
1655 #[test]
1656 fn delete_part_removes_part() {
1657 let mut score = default_engine_score();
1658 apply_command(&Command::AddPart(AddPartCmd {
1659 name: "Violin".into(), short_name: "V.".into(), clefs: vec!["Treble".into()],
1660 midi_channel: 0, midi_program: 0,
1661 }), &mut score).unwrap();
1662 let before = score.parts.len();
1663 apply_command(&Command::DeletePart(DeletePartCmd { part_index: 0 }), &mut score).unwrap();
1664 assert_eq!(score.parts.len(), before - 1);
1665 }
1666
1667 #[test]
1668 fn delete_part_out_of_range_returns_err() {
1669 let mut score = default_engine_score();
1670 assert!(apply_command(
1671 &Command::DeletePart(DeletePartCmd { part_index: 99 }),
1672 &mut score
1673 ).is_err());
1674 }
1675
1676 #[test]
1677 fn delete_part_undo_restores_part() {
1678 let mut stack = CommandStack::new(50);
1679 let mut score = default_engine_score();
1680 apply_command(&Command::AddPart(AddPartCmd {
1681 name: "Violin".into(), short_name: "V.".into(), clefs: vec!["Treble".into()],
1682 midi_channel: 0, midi_program: 0,
1683 }), &mut score).unwrap();
1684 let before = score.parts.len();
1685 stack.execute(Command::DeletePart(DeletePartCmd { part_index: 0 }), &mut score).unwrap();
1686 assert_eq!(score.parts.len(), before - 1);
1687 stack.undo(&mut score).unwrap();
1688 assert_eq!(score.parts.len(), before);
1689 }
1690
1691 #[test]
1692 fn set_metadata_updates_title() {
1693 let mut score = default_engine_score();
1694 apply_command(&Command::SetMetadata(SetMetadataCmd {
1695 title: Some("New Title".into()),
1696 ..Default::default()
1697 }), &mut score).unwrap();
1698 assert_eq!(score.metadata.title, "New Title");
1699 }
1700
1701 #[test]
1702 fn set_metadata_none_fields_skipped() {
1703 let mut score = default_engine_score();
1704 let original_composer = score.metadata.composer.clone();
1705 apply_command(&Command::SetMetadata(SetMetadataCmd {
1706 title: Some("X".into()),
1707 ..Default::default()
1708 }), &mut score).unwrap();
1709 assert_eq!(score.metadata.composer, original_composer);
1710 }
1711
1712 #[test]
1713 fn set_volta_sets_bracket() {
1714 use crate::model::score::VoltaBracket;
1715 let mut score = default_engine_score();
1716 let volta = VoltaBracket { number: 1, kind: "begin_end".into() };
1717 apply_command(&Command::SetVolta(SetVoltaCmd {
1718 measure_index: 0,
1719 volta: Some(volta.clone()),
1720 }), &mut score).unwrap();
1721 assert!(score.parts[0].staves[0].measures[0].volta.is_some());
1722 }
1723
1724 #[test]
1725 fn set_volta_none_clears_bracket() {
1726 use crate::model::score::VoltaBracket;
1727 let mut score = default_engine_score();
1728 score.parts[0].staves[0].measures[0].volta =
1729 Some(VoltaBracket { number: 1, kind: "begin_end".into() });
1730 apply_command(&Command::SetVolta(SetVoltaCmd {
1731 measure_index: 0, volta: None,
1732 }), &mut score).unwrap();
1733 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
1734 }
1735
1736 #[test]
1737 fn set_volta_undo_restores_old() {
1738 use crate::model::score::VoltaBracket;
1739 let mut stack = CommandStack::new(50);
1740 let mut score = default_engine_score();
1741 stack.execute(Command::SetVolta(SetVoltaCmd {
1742 measure_index: 0,
1743 volta: Some(VoltaBracket { number: 1, kind: "begin_end".into() }),
1744 }), &mut score).unwrap();
1745 stack.undo(&mut score).unwrap();
1746 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
1747 }
1748
1749 #[test]
1750 fn set_clef_updates_staff_clef() {
1751 use crate::model::notation::Clef;
1752 let mut score = default_engine_score();
1753 apply_command(&Command::SetClef(SetClefCmd {
1754 part_index: 0, staff_index: 0, clef: Clef::Bass,
1755 }), &mut score).unwrap();
1756 assert_eq!(score.parts[0].staves[0].clef, Clef::Bass);
1757 }
1758
1759 #[test]
1760 fn set_clef_out_of_range_returns_err() {
1761 use crate::model::notation::Clef;
1762 let mut score = default_engine_score();
1763 assert!(apply_command(&Command::SetClef(SetClefCmd {
1764 part_index: 99, staff_index: 0, clef: Clef::Bass,
1765 }), &mut score).is_err());
1766 }
1767
1768 #[test]
1769 fn set_part_name_updates_name() {
1770 let mut score = default_engine_score();
1771 apply_command(&Command::SetPartName(SetPartNameCmd {
1772 part_index: 0,
1773 name: "Violin".into(),
1774 short_name: "Vln.".into(),
1775 }), &mut score).unwrap();
1776 assert_eq!(score.parts[0].name, "Violin");
1777 assert_eq!(score.parts[0].short_name, "Vln.");
1778 }
1779
1780 #[test]
1781 fn set_part_name_undo_restores_old() {
1782 let mut stack = CommandStack::new(50);
1783 let mut score = default_engine_score();
1784 let original = score.parts[0].name.clone();
1785 stack.execute(Command::SetPartName(SetPartNameCmd {
1786 part_index: 0, name: "Flute".into(), short_name: "Fl.".into(),
1787 }), &mut score).unwrap();
1788 stack.undo(&mut score).unwrap();
1789 assert_eq!(score.parts[0].name, original);
1790 }
1791
1792 #[test]
1793 fn set_metadata_undo_restores_old_title() {
1794 let mut stack = CommandStack::new(50);
1795 let mut score = default_engine_score();
1796 let original = score.metadata.title.clone();
1797 stack.execute(Command::SetMetadata(SetMetadataCmd {
1798 title: Some("Changed".into()),
1799 ..Default::default()
1800 }), &mut score).unwrap();
1801 assert_ne!(score.metadata.title, original);
1802 stack.undo(&mut score).unwrap();
1803 assert_eq!(score.metadata.title, original);
1804 }
1805
1806 #[test]
1807 fn set_midi_instrument_updates_channel_and_program() {
1808 let mut score = default_engine_score();
1809 apply_command(&Command::SetMidiInstrument(SetMidiInstrumentCmd {
1810 part_index: 0, midi_channel: 2, midi_program: 40,
1811 }), &mut score).unwrap();
1812 assert_eq!(score.parts[0].midi_channel, 2);
1813 assert_eq!(score.parts[0].midi_program, 40);
1814 }
1815
1816 #[test]
1817 fn set_midi_instrument_clamps_channel_to_15() {
1818 let mut score = default_engine_score();
1819 apply_command(&Command::SetMidiInstrument(SetMidiInstrumentCmd {
1820 part_index: 0, midi_channel: 20, midi_program: 0,
1821 }), &mut score).unwrap();
1822 assert_eq!(score.parts[0].midi_channel, 15);
1823 }
1824
1825 #[test]
1826 fn set_midi_instrument_undo_restores_old() {
1827 let mut stack = CommandStack::new(50);
1828 let mut score = default_engine_score();
1829 score.parts[0].midi_channel = 3;
1830 score.parts[0].midi_program = 10;
1831 stack.execute(Command::SetMidiInstrument(SetMidiInstrumentCmd {
1832 part_index: 0, midi_channel: 9, midi_program: 114,
1833 }), &mut score).unwrap();
1834 stack.undo(&mut score).unwrap();
1835 assert_eq!(score.parts[0].midi_channel, 3);
1836 assert_eq!(score.parts[0].midi_program, 10);
1837 }
1838
1839 #[test]
1840 fn set_transpose_updates_staff() {
1841 let mut score = default_engine_score();
1842 apply_command(&Command::SetTranspose(SetTransposeCmd {
1843 part_index: 0, staff_index: 0, semitones: -2,
1844 }), &mut score).unwrap();
1845 assert_eq!(score.parts[0].staves[0].transpose_semitones, -2);
1846 }
1847
1848 #[test]
1849 fn set_transpose_out_of_range_returns_err() {
1850 let mut score = default_engine_score();
1851 assert!(apply_command(&Command::SetTranspose(SetTransposeCmd {
1852 part_index: 99, staff_index: 0, semitones: -2,
1853 }), &mut score).is_err());
1854 }
1855
1856 #[test]
1857 fn set_tempo_at_measure_sets_tempo() {
1858 let mut score = default_engine_score();
1859 apply_command(&Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
1860 measure_index: 0, bpm: Some(80),
1861 }), &mut score).unwrap();
1862 assert_eq!(score.parts[0].staves[0].measures[0].tempo, Some(80));
1863 }
1864
1865 #[test]
1866 fn set_tempo_at_measure_none_clears_tempo() {
1867 let mut score = default_engine_score();
1868 score.parts[0].staves[0].measures[0].tempo = Some(120);
1869 apply_command(&Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
1870 measure_index: 0, bpm: None,
1871 }), &mut score).unwrap();
1872 assert!(score.parts[0].staves[0].measures[0].tempo.is_none());
1873 }
1874
1875 #[test]
1878 fn batch_execute_two_commands_single_undo() {
1879 let mut stack = CommandStack::new(50);
1880 let mut score = default_engine_score();
1881 let original_bpm = score.settings.tempo_bpm;
1882 stack.batch_execute(vec![
1883 Command::SetTempo(SetTempoCmd { bpm: 160 }),
1884 Command::SetTempo(SetTempoCmd { bpm: 180 }),
1885 ], &mut score).unwrap();
1886 assert_eq!(score.settings.tempo_bpm, 180);
1887 stack.undo(&mut score).unwrap();
1888 assert_eq!(score.settings.tempo_bpm, original_bpm);
1889 }
1890
1891 #[test]
1892 fn batch_execute_partial_failure_rollback() {
1893 let mut stack = CommandStack::new(50);
1894 let mut score = default_engine_score();
1895 let original_bpm = score.settings.tempo_bpm;
1896 let result = stack.batch_execute(vec![
1897 Command::SetTempo(SetTempoCmd { bpm: 160 }),
1898 Command::DeleteNote(DeleteNoteCmd {
1899 note_id: "nonexistent".into(),
1900 part_index: 99, staff_index: 0, measure_index: 0, voice: 0,
1901 }),
1902 ], &mut score);
1903 assert!(result.is_err());
1904 assert_eq!(score.settings.tempo_bpm, original_bpm);
1905 }
1906
1907 #[test]
1908 fn batch_execute_empty_is_noop() {
1909 let mut stack = CommandStack::new(50);
1910 let mut score = default_engine_score();
1911 stack.batch_execute(vec![], &mut score).unwrap();
1912 assert!(!stack.can_undo());
1913 }
1914
1915 #[test]
1918 fn batch_label_used_as_command_key() {
1919 let cmd = Command::Batch(BatchCmd {
1920 commands: vec![],
1921 label: Some("ApplyAI".to_string()),
1922 });
1923 assert_eq!(command_key(&cmd), "ApplyAI");
1924 }
1925
1926 #[test]
1927 fn batch_no_label_key_is_batch() {
1928 let cmd = Command::Batch(BatchCmd { commands: vec![], label: None });
1929 assert_eq!(command_key(&cmd), "Batch");
1930 }
1931
1932 #[test]
1933 fn batch_label_survives_json_roundtrip() {
1934 let cmd = Command::Batch(BatchCmd {
1935 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 120 })],
1936 label: Some("PasteSelection".to_string()),
1937 });
1938 let json = serde_json::to_string(&cmd).unwrap();
1939 let cmd2: Command = serde_json::from_str(&json).unwrap();
1940 assert_eq!(command_key(&cmd2), "PasteSelection");
1941 }
1942
1943 #[test]
1944 fn batch_label_in_undo_key() {
1945 let mut stack = CommandStack::new(50);
1946 let mut score = default_engine_score();
1947 let cmd = Command::Batch(BatchCmd {
1948 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 140 })],
1949 label: Some("ApplyAI".to_string()),
1950 });
1951 stack.execute(cmd, &mut score).unwrap();
1952 assert_eq!(stack.undo_key(), Some("ApplyAI".to_string()));
1953 }
1954
1955 #[test]
1956 fn undo_returns_change_hint() {
1957 use crate::model::change_hint::ChangeScope;
1958 let mut stack = CommandStack::new(50);
1959 let mut score = default_engine_score();
1960 stack.execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score).unwrap();
1961 let hint = stack.undo(&mut score).unwrap();
1962 assert_eq!(hint.scope, ChangeScope::Global);
1963 assert!(hint.playback_dirty);
1964 }
1965
1966 #[test]
1967 fn redo_returns_change_hint() {
1968 use crate::model::change_hint::ChangeScope;
1969 let mut stack = CommandStack::new(50);
1970 let mut score = default_engine_score();
1971 stack.execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score).unwrap();
1972 stack.undo(&mut score).unwrap();
1973 let hint = stack.redo(&mut score).unwrap();
1974 assert_eq!(hint.scope, ChangeScope::Global);
1975 assert!(hint.playback_dirty);
1976 }
1977
1978 #[test]
1981 fn toggle_slur_sets_start_and_end() {
1982 let mut score = default_engine_score();
1983 let cmd = Command::AddNote(AddNoteCmd {
1984 part_index: 0, staff_index: 0, measure_index: 0, voice: 0,
1985 position: 0, pitch: Some(Pitch::new(Step::C, 4)),
1986 duration: Duration::Quarter, dot_count: 0, is_rest: false, tuplet: None,
1987 });
1988 apply_command(&cmd, &mut score).unwrap();
1989 apply_command(&Command::AddNote(AddNoteCmd {
1990 part_index: 0, staff_index: 0, measure_index: 0, voice: 0,
1991 position: 1, pitch: Some(Pitch::new(Step::D, 4)),
1992 duration: Duration::Quarter, dot_count: 0, is_rest: false, tuplet: None,
1993 }), &mut score).unwrap();
1994 let start = NoteAddr { part: 0, staff: 0, measure: 0, voice: 0, note: 0 };
1995 let end = NoteAddr { part: 0, staff: 0, measure: 0, voice: 0, note: 1 };
1996 apply_command(&Command::ToggleSlur(ToggleSlurCmd { start: start.clone(), end: end.clone() }), &mut score).unwrap();
1997 assert!(score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
1998 assert!(score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
1999 apply_command(&Command::ToggleSlur(ToggleSlurCmd { start, end }), &mut score).unwrap();
2001 assert!(!score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
2002 assert!(!score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
2003 }
2004
2005 #[test]
2008 fn add_staff_appends_staff_with_correct_measure_count() {
2009 let mut score = default_engine_score();
2010 let before = score.parts[0].staves.len();
2011 let measure_count = score.parts[0].staves[0].measures.len();
2012 apply_command(&Command::AddStaff(AddStaffCmd { part_index: 0, clef: Clef::Bass }), &mut score).unwrap();
2013 assert_eq!(score.parts[0].staves.len(), before + 1);
2014 let new_staff = score.parts[0].staves.last().unwrap();
2015 assert_eq!(new_staff.measures.len(), measure_count);
2016 }
2017
2018 #[test]
2019 fn add_staff_out_of_range_returns_err() {
2020 let mut score = default_engine_score();
2021 let result = apply_command(&Command::AddStaff(AddStaffCmd { part_index: 99, clef: Clef::Treble }), &mut score);
2022 assert!(result.is_err());
2023 }
2024
2025 #[test]
2026 fn delete_staff_removes_extra_staff() {
2027 let mut score = default_engine_score();
2028 apply_command(&Command::AddStaff(AddStaffCmd { part_index: 0, clef: Clef::Bass }), &mut score).unwrap();
2029 assert_eq!(score.parts[0].staves.len(), 2);
2030 apply_command(&Command::DeleteStaff(DeleteStaffCmd { part_index: 0, staff_index: 1 }), &mut score).unwrap();
2031 assert_eq!(score.parts[0].staves.len(), 1);
2032 }
2033
2034 #[test]
2035 fn delete_last_staff_returns_err() {
2036 let mut score = default_engine_score();
2037 assert_eq!(score.parts[0].staves.len(), 1);
2038 let result = apply_command(&Command::DeleteStaff(DeleteStaffCmd { part_index: 0, staff_index: 0 }), &mut score);
2039 assert!(result.is_err());
2040 }
2041
2042 #[test]
2045 fn set_tuplet_assigns_and_clears() {
2046 use crate::model::notation::TupletInfo;
2047 let mut score = default_engine_score();
2048 apply_command(&Command::AddNote(AddNoteCmd {
2049 part_index: 0, staff_index: 0, measure_index: 0, voice: 0,
2050 position: 0, pitch: Some(Pitch::new(Step::C, 4)),
2051 duration: Duration::Quarter, dot_count: 0, is_rest: false, tuplet: None,
2052 }), &mut score).unwrap();
2053 let ti = TupletInfo { actual_notes: 3, normal_notes: 2 };
2054 apply_command(&Command::SetTuplet(SetTupletCmd {
2055 part_index: 0, staff_index: 0, measure_index: 0, voice_index: 0, note_index: 0,
2056 tuplet: Some(ti.clone()),
2057 }), &mut score).unwrap();
2058 assert_eq!(score.parts[0].staves[0].measures[0].voices[0][0].tuplet, Some(ti));
2059 apply_command(&Command::SetTuplet(SetTupletCmd {
2060 part_index: 0, staff_index: 0, measure_index: 0, voice_index: 0, note_index: 0,
2061 tuplet: None,
2062 }), &mut score).unwrap();
2063 assert!(score.parts[0].staves[0].measures[0].voices[0][0].tuplet.is_none());
2064 }
2065
2066 #[test]
2069 fn respell_score_cmd_changes_all_pitches() {
2070 use crate::model::pitch::Step;
2071 let mut score = default_engine_score();
2072 apply_command(&Command::AddNote(AddNoteCmd {
2073 part_index: 0, staff_index: 0, measure_index: 0, voice: 0,
2074 position: 0,
2075 pitch: Some(Pitch::with_alter(Step::C, 4, 1)), duration: Duration::Quarter, dot_count: 0, is_rest: false, tuplet: None,
2077 }), &mut score).unwrap();
2078 apply_command(&Command::RespellScore(RespellScoreCmd { prefer_flat: true }), &mut score).unwrap();
2079 let pitch = &score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
2080 assert_eq!(pitch.step, Step::D);
2081 assert_eq!(pitch.alter, -1); }
2083}