1use super::change_hint::{ChangeHint, ChangeScope};
2use super::duration::Duration;
3use super::fragment::{
4 MIN_SUPPORTED_SCORE_FRAGMENT_CONTRACT_VERSION, SCORE_FRAGMENT_CONTRACT_VERSION, ScoreFragment,
5 ScoreFragmentSelection, extract_score_fragment,
6};
7use super::notation::{
8 Articulation, Barline, ChordSymbol, Clef, CrossStaff, Dynamic, FiguredBassFigure,
9 GuitarTechnique, HairpinKind, KeySignature, Lyric, NoteHead, OttavaKind, StyledText,
10 TablatureConfig, TimeSignature, TupletInfo,
11};
12use super::pitch::Pitch;
13use super::score::{
14 HarpPedalDiagram, InstrumentDefinition, InstrumentRange, Measure, NotationSpanner,
15 NotationSpannerKind, Note, NoteAddr, ObjectStyleOverride, Part, PartGroup,
16 PercussionInstrument, RegionalTranspositionTarget, Score, ScoreTemplate, ScoreView, Staff,
17 StaffKind, StaffPresentation, ViewStyleOverride, respell_score, respell_score_to_key,
18 transpose_staff_region_checked,
19};
20use super::validate::validate;
21use crate::Error;
22use serde::{Deserialize, Serialize};
23use std::collections::BTreeSet;
24use uuid::Uuid;
25
26mod range_commands;
27mod spanner_remap;
28mod structural_commands;
29
30use self::range_commands::{apply_paste_range, apply_paste_voice};
31use self::spanner_remap::{
32 clear_legacy_spanner_endpoints, note_at, prune_orphaned_spanners, remap_spanners,
33};
34use self::structural_commands::{apply_join_measures, apply_split_measure};
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(tag = "type", rename_all = "snake_case")]
38pub enum Command {
39 AddNote(AddNoteCmd),
40 AddPitch(AddPitchCmd),
41 SetDuration(SetDurationCmd),
42 DeleteNote(DeleteNoteCmd),
43 AddMeasure(AddMeasureCmd),
44 DeleteMeasure(DeleteMeasureCmd),
45 SetTempo(SetTempoCmd),
46 NewScore(NewScoreCmd),
47 AddHairpin(AddHairpinCmd),
48 ToggleTie(ToggleTieCmd),
49 SetDynamic(SetDynamicCmd),
50 ToggleArticulation(ToggleArticulationCmd),
51 SetKeySignature(SetKeySignatureCmd),
52 SetTimeSignature(SetTimeSignatureCmd),
53 SetBarline(SetBarlineCmd),
54 AddPart(AddPartCmd),
55 DeletePart(DeletePartCmd),
56 ReorderParts(ReorderPartsCmd),
57 SetMetadata(SetMetadataCmd),
58 SetRehearsalMark(SetRehearsalMarkCmd),
59 SetNavigationMark(SetNavigationMarkCmd),
60 SetChordSymbol(SetChordSymbolCmd),
61 SetHarmonyRange(SetHarmonyRangeCmd),
62 SetFiguredBass(SetFiguredBassCmd),
63 SetHarpPedalDiagrams(SetHarpPedalDiagramsCmd),
64 SetGrace(SetGraceCmd),
65 SetOttava(SetOttavaCmd),
66 SetLyric(SetLyricCmd),
67 SetMultiRest(SetMultiRestCmd),
68 AddPedal(AddPedalCmd),
69 SetVolta(SetVoltaCmd),
70 SetClef(SetClefCmd),
71 SetPartName(SetPartNameCmd),
72 SetMidiInstrument(SetMidiInstrumentCmd),
73 SetPercussionKit(SetPercussionKitCmd),
74 SetInstrumentDefinition(SetInstrumentDefinitionCmd),
75 SetMeasureInstrumentChange(SetMeasureInstrumentChangeCmd),
76 SetMeasureTablatureChange(SetMeasureTablatureChangeCmd),
77 UpsertScoreView(UpsertScoreViewCmd),
78 RemoveScoreView(RemoveScoreViewCmd),
79 SetTranspose(SetTransposeCmd),
80 TransposeStaffRegion(TransposeStaffRegionCmd),
81 SetTempoAtMeasure(SetTempoAtMeasureCmd),
82 SetTempoRampAtMeasure(SetTempoRampAtMeasureCmd),
83 PasteVoice(PasteVoiceCmd),
84 PasteRange(PasteRangeCmd),
85 PasteScoreFragment(PasteScoreFragmentCmd),
86 ExchangeVoices(ExchangeVoicesCmd),
87 MoveOrCopyVoiceRange(MoveOrCopyVoiceRangeCmd),
88 SplitMeasure(SplitMeasureCmd),
89 JoinMeasures(JoinMeasuresCmd),
90 ImplodeStaves(ImplodeStavesCmd),
91 ExplodeVoices(ExplodeVoicesCmd),
92 ExplodeChordPitches(ExplodeChordPitchesCmd),
93 ScaleVoiceRange(ScaleVoiceRangeCmd),
94 SetSystemBreak(SetSystemBreakCmd),
95 SetPageBreak(SetPageBreakCmd),
96 SetSectionBreak(SetSectionBreakCmd),
97 ToggleSlur(ToggleSlurCmd),
98 AddStaff(AddStaffCmd),
99 DeleteStaff(DeleteStaffCmd),
100 SetTuplet(SetTupletCmd),
101 RespellScore(RespellScoreCmd),
102 RespellScoreToKey(RespellScoreToKeyCmd),
103 SetStem(SetStemCmd),
104 SetArpeggio(SetArpeggioCmd),
105 SetTechniqueText(SetTechniqueTextCmd),
106 SetFingering(SetFingeringCmd),
107 SetFingerings(SetFingeringsCmd),
108 SetStringNumber(SetStringNumberCmd),
109 SetTabPosition(SetTabPositionCmd),
110 SetTablatureConfig(SetTablatureConfigCmd),
111 SetStaffPresentation(SetStaffPresentationCmd),
112 SetNoteHead(SetNoteHeadCmd),
113 SetCue(SetCueCmd),
114 SetUnpitched(SetUnpitchedCmd),
115 SetInstrumentId(SetInstrumentIdCmd),
116 SetNotePlacement(SetNotePlacementCmd),
117 SetGuitarTechnique(SetGuitarTechniqueCmd),
118 SetGuitarBendAlter(SetGuitarBendAlterCmd),
119 SetGuitarBendCurve(SetGuitarBendCurveCmd),
120 SetExpressionText(SetExpressionTextCmd),
121 SetMeasureText(SetMeasureTextCmd),
122 SetScoreText(SetScoreTextCmd),
123 SetScoreStyleOverrides(SetScoreStyleOverridesCmd),
124 SetObjectStyleOverrides(SetObjectStyleOverridesCmd),
125 ToggleTrillLine(ToggleTrillLineCmd),
126 SetGlissando(SetGlissandoCmd),
127 SetCrossStaff(SetCrossStaffCmd),
128 SetPartGroup(SetPartGroupCmd),
129 AddSpanner(AddSpannerCmd),
130 UpdateSpanner(UpdateSpannerCmd),
131 RemoveSpanner(RemoveSpannerCmd),
132 Batch(BatchCmd),
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct AddNoteCmd {
137 pub part_index: usize,
138 pub staff_index: usize,
139 pub measure_index: usize,
140 pub voice: usize,
141 pub position: usize,
142 pub pitch: Option<Pitch>,
143 pub duration: Duration,
144 pub dot_count: u8,
145 pub is_rest: bool,
146 #[serde(default)]
147 pub tuplet: Option<TupletInfo>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct AddPitchCmd {
152 pub part_index: usize,
153 pub staff_index: usize,
154 pub measure_index: usize,
155 pub voice: usize,
156 pub note_index: usize,
157 pub pitch: Pitch,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct SetDurationCmd {
162 pub part_index: usize,
163 pub staff_index: usize,
164 pub measure_index: usize,
165 pub voice: usize,
166 pub note_index: usize,
167 pub duration: Duration,
168 #[serde(default)]
169 pub dot_count: u8,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct AddSpannerCmd {
175 pub spanner: NotationSpanner,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct UpdateSpannerCmd {
181 pub spanner: NotationSpanner,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct RemoveSpannerCmd {
187 pub id: String,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct DeleteNoteCmd {
192 pub note_id: String,
193 pub part_index: usize,
194 pub staff_index: usize,
195 pub measure_index: usize,
196 pub voice: usize,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct AddMeasureCmd {
201 pub after_index: usize,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct DeleteMeasureCmd {
206 pub measure_index: usize,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct SetTempoCmd {
211 pub bpm: u16,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct NewScoreCmd {
216 pub title: String,
217 pub composer: String,
218 pub tempo_bpm: u16,
219 pub time_numerator: u8,
220 pub time_denominator: u8,
221 pub key_fifths: i8,
222 pub measure_count: u32,
223 #[serde(default)]
225 pub template: Option<ScoreTemplate>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct AddHairpinCmd {
230 pub part_index: usize,
231 pub staff_index: usize,
232 pub measure_index: usize,
233 pub voice: usize,
234 pub start_note_idx: usize,
235 pub end_note_idx: usize,
236 pub kind: HairpinKind,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct ToggleTieCmd {
241 pub part_index: usize,
242 pub staff_index: usize,
243 pub measure_index: usize,
244 pub voice: usize,
245 pub note_index: usize,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct SetDynamicCmd {
250 pub part_index: usize,
251 pub staff_index: usize,
252 pub measure_index: usize,
253 pub voice: usize,
254 pub note_index: usize,
255 pub dynamic: Option<Dynamic>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct ToggleArticulationCmd {
260 pub part_index: usize,
261 pub staff_index: usize,
262 pub measure_index: usize,
263 pub voice: usize,
264 pub note_index: usize,
265 pub articulation: Articulation,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct SetKeySignatureCmd {
270 pub fifths: i8,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct SetTimeSignatureCmd {
275 pub numerator: u8,
276 pub denominator: u8,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct SetBarlineCmd {
281 pub measure_index: usize,
282 pub side: String,
284 pub barline: Barline,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct AddPartCmd {
289 pub name: String,
290 pub short_name: String,
291 pub clefs: Vec<String>,
293 #[serde(default)]
295 pub midi_channel: u8,
296 #[serde(default)]
298 pub midi_program: u8,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct DeletePartCmd {
303 pub part_index: usize,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct ReorderPartsCmd {
312 pub order: Vec<usize>,
313}
314
315#[derive(Debug, Clone, Default, Serialize, Deserialize)]
316pub struct SetMetadataCmd {
317 pub title: Option<String>,
318 pub composer: Option<String>,
319 pub lyricist: Option<String>,
320 pub copyright: Option<String>,
321 pub work_number: Option<String>,
322 pub movement_title: Option<String>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct SetRehearsalMarkCmd {
327 pub measure_index: usize,
328 pub text: Option<String>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct SetNavigationMarkCmd {
333 pub measure_index: usize,
334 pub mark: Option<String>,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct SetChordSymbolCmd {
341 pub part_index: usize,
342 pub staff_index: usize,
343 pub measure_index: usize,
344 pub voice: usize,
345 pub note_index: usize,
346 pub chord: Option<ChordSymbol>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct SetHarmonyRangeCmd {
352 pub part_index: usize,
353 pub staff_index: usize,
354 pub measure_index: usize,
355 pub voice: usize,
356 pub note_index: usize,
357 pub end: Option<NoteAddr>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct SetFiguredBassCmd {
363 pub measure_index: usize,
364 pub figures: Vec<FiguredBassFigure>,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct SetHarpPedalDiagramsCmd {
370 pub part_index: usize,
371 pub staff_index: usize,
372 pub measure_index: usize,
373 pub diagrams: Vec<HarpPedalDiagram>,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct SetGraceCmd {
378 pub part_index: usize,
379 pub staff_index: usize,
380 pub measure_index: usize,
381 pub voice: usize,
382 pub note_index: usize,
383 pub is_grace: bool,
384 pub slash: bool,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct SetOttavaCmd {
390 pub part_index: usize,
391 pub staff_index: usize,
392 pub measure_index: usize,
393 pub voice: usize,
394 pub note_index: usize,
395 pub ottava_start: Option<OttavaKind>,
396 pub ottava_end: bool,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct SetLyricCmd {
401 pub part_index: usize,
402 pub staff_index: usize,
403 pub measure_index: usize,
404 pub voice: usize,
405 pub note_index: usize,
406 pub lyric: Option<Lyric>,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct SetMultiRestCmd {
411 pub measure_index: usize,
412 pub count: Option<u8>,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct SetVoltaCmd {
417 pub measure_index: usize,
418 pub volta: Option<super::score::VoltaBracket>,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct SetClefCmd {
423 pub part_index: usize,
424 pub staff_index: usize,
425 pub clef: Clef,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct SetPartNameCmd {
430 pub part_index: usize,
431 pub name: String,
432 pub short_name: String,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct SetMidiInstrumentCmd {
437 pub part_index: usize,
438 pub midi_channel: u8,
440 pub midi_program: u8,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct SetPercussionKitCmd {
447 pub part_index: usize,
448 pub instruments: Vec<PercussionInstrument>,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct SetInstrumentDefinitionCmd {
454 pub part_index: usize,
455 pub definition: Option<InstrumentDefinition>,
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct SetMeasureInstrumentChangeCmd {
461 pub part_index: usize,
462 pub staff_index: usize,
463 pub measure_index: usize,
464 pub definition: Option<InstrumentDefinition>,
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct SetMeasureTablatureChangeCmd {
470 pub part_index: usize,
471 pub staff_index: usize,
472 pub measure_index: usize,
473 pub config: Option<TablatureConfig>,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct UpsertScoreViewCmd {
479 pub view: ScoreView,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct RemoveScoreViewCmd {
485 pub id: String,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct SetTransposeCmd {
490 pub part_index: usize,
491 pub staff_index: usize,
492 pub semitones: i8,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct TransposeStaffRegionCmd {
499 pub part_index: usize,
500 pub staff_index: usize,
501 pub start_measure: usize,
503 pub end_measure: usize,
505 pub semitones: i8,
506 pub target: RegionalTranspositionTarget,
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct SetTempoAtMeasureCmd {
511 pub measure_index: usize,
512 pub bpm: Option<u16>,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct SetTempoRampAtMeasureCmd {
519 pub measure_index: usize,
520 pub target_bpm: Option<u16>,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
524pub struct PasteVoiceCmd {
525 pub part_index: usize,
526 pub staff_index: usize,
527 pub measure_index: usize,
528 pub voice_index: usize,
529 pub notes: Vec<Note>,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct SetSystemBreakCmd {
535 pub measure_index: usize,
536 pub value: bool,
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
540pub struct SetPageBreakCmd {
541 pub measure_index: usize,
542 pub value: bool,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct SetSectionBreakCmd {
548 pub measure_index: usize,
549 pub value: bool,
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct BatchCmd {
555 pub commands: Vec<Command>,
556 #[serde(default)]
559 pub label: Option<String>,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct AddPedalCmd {
564 pub part_index: usize,
565 pub staff_index: usize,
566 pub measure_index: usize,
567 pub voice: usize,
568 pub start_note_idx: usize,
569 pub end_note_idx: usize,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct PasteRangeCmd {
578 pub part_index: usize,
579 pub staff_index: usize,
580 pub voice_index: usize,
581 pub target_measure: usize,
582 pub measures: Vec<Vec<Note>>,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct PasteScoreFragmentCmd {
593 pub fragment: ScoreFragment,
594 pub target: NoteAddr,
595 #[serde(default)]
596 pub policy: ScoreFragmentPastePolicy,
597}
598
599#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
601#[serde(rename_all = "snake_case")]
602pub enum ScoreFragmentPastePolicy {
603 #[default]
605 Replace,
606 Merge,
609}
610
611#[derive(Debug, Clone, Serialize, Deserialize)]
616pub struct ExchangeVoicesCmd {
617 pub part_index: usize,
618 pub staff_index: usize,
619 pub start_measure: usize,
620 pub end_measure: usize,
621 pub first_voice: usize,
622 pub second_voice: usize,
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize)]
630pub struct MoveOrCopyVoiceRangeCmd {
631 pub source_start: NoteAddr,
632 pub source_end: NoteAddr,
633 pub target: NoteAddr,
634 #[serde(default)]
635 pub move_source: bool,
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct SplitMeasureCmd {
641 pub measure_index: usize,
642 pub split_at_beats: f64,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize)]
647pub struct JoinMeasuresCmd {
648 pub measure_index: usize,
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize)]
658pub struct ImplodeStavesCmd {
659 pub part_index: usize,
660 pub source_staves: Vec<usize>,
661 pub target_staff: usize,
662 pub start_measure: usize,
663 pub end_measure: usize,
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct ExplodeVoicesCmd {
674 pub part_index: usize,
675 pub source_staff: usize,
676 pub target_staves: Vec<usize>,
677 pub start_measure: usize,
678 pub end_measure: usize,
679}
680
681#[derive(Debug, Clone, Serialize, Deserialize)]
689pub struct ExplodeChordPitchesCmd {
690 pub part_index: usize,
691 pub source_staff: usize,
692 pub target_staves: Vec<usize>,
693 pub start_measure: usize,
694 pub end_measure: usize,
695}
696
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(rename_all = "snake_case")]
700pub enum DurationScale {
701 Half,
702 Double,
703}
704
705#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
707#[serde(rename_all = "snake_case")]
708pub enum TupletScalePolicy {
709 #[default]
711 PreserveRatio,
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
721pub struct ScaleVoiceRangeCmd {
722 pub part_index: usize,
723 pub staff_index: usize,
724 pub voice: usize,
725 pub start_measure: usize,
726 pub end_measure: usize,
727 pub scale: DurationScale,
728 #[serde(default)]
729 pub tuplet_policy: TupletScalePolicy,
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
734pub struct ToggleSlurCmd {
735 pub start: NoteAddr,
736 pub end: NoteAddr,
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize)]
741pub struct SetPartGroupCmd {
742 pub group: Option<PartGroup>,
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize)]
748pub struct ToggleTrillLineCmd {
749 pub start: NoteAddr,
750 pub end: NoteAddr,
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize)]
758pub struct AddStaffCmd {
759 pub part_index: usize,
760 pub clef: Clef,
761}
762
763#[derive(Debug, Clone, Serialize, Deserialize)]
765pub struct DeleteStaffCmd {
766 pub part_index: usize,
767 pub staff_index: usize,
768}
769
770#[derive(Debug, Clone, Serialize, Deserialize)]
772pub struct SetTupletCmd {
773 pub part_index: usize,
774 pub staff_index: usize,
775 pub measure_index: usize,
776 pub voice_index: usize,
777 pub note_index: usize,
778 pub tuplet: Option<TupletInfo>,
780}
781
782#[derive(Debug, Clone, Serialize, Deserialize)]
784pub struct SetStemCmd {
785 pub part_index: usize,
786 pub staff_index: usize,
787 pub measure_index: usize,
788 pub voice_index: usize,
789 pub note_index: usize,
790 pub stem_up: Option<bool>,
792}
793
794#[derive(Debug, Clone, Serialize, Deserialize)]
795pub struct SetArpeggioCmd {
796 pub part_index: usize,
797 pub staff_index: usize,
798 pub measure_index: usize,
799 pub voice_index: usize,
800 pub note_index: usize,
801 pub direction: Option<bool>,
803}
804
805#[derive(Debug, Clone, Serialize, Deserialize)]
807pub struct SetTechniqueTextCmd {
808 pub part_index: usize,
809 pub staff_index: usize,
810 pub measure_index: usize,
811 pub voice: usize,
812 pub note_index: usize,
813 pub text: Option<String>,
815}
816
817#[derive(Debug, Clone, Serialize, Deserialize)]
818pub struct SetGlissandoCmd {
819 pub part_index: usize,
820 pub staff_index: usize,
821 pub measure_index: usize,
822 pub voice: usize,
823 pub note_index: usize,
824 pub start: bool,
825 pub end: bool,
826}
827
828#[derive(Debug, Clone, Serialize, Deserialize)]
829pub struct SetCrossStaffCmd {
830 pub part_index: usize,
831 pub staff_index: usize,
832 pub measure_index: usize,
833 pub voice: usize,
834 pub note_index: usize,
835 pub placement: Option<CrossStaff>,
836}
837
838#[derive(Debug, Clone, Serialize, Deserialize)]
840pub struct SetFingeringCmd {
841 pub part_index: usize,
842 pub staff_index: usize,
843 pub measure_index: usize,
844 pub voice: usize,
845 pub note_index: usize,
846 pub fingering: Option<u8>,
848}
849
850#[derive(Debug, Clone, Serialize, Deserialize)]
853pub struct SetFingeringsCmd {
854 pub part_index: usize,
855 pub staff_index: usize,
856 pub measure_index: usize,
857 pub voice: usize,
858 pub note_index: usize,
859 pub fingerings: Vec<u8>,
860}
861
862#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct SetStringNumberCmd {
865 pub part_index: usize,
866 pub staff_index: usize,
867 pub measure_index: usize,
868 pub voice: usize,
869 pub note_index: usize,
870 pub string_number: Option<u8>,
872}
873
874#[derive(Debug, Clone, Serialize, Deserialize)]
876pub struct SetTabPositionCmd {
877 pub part_index: usize,
878 pub staff_index: usize,
879 pub measure_index: usize,
880 pub voice: usize,
881 pub note_index: usize,
882 pub position: Option<super::notation::TabPosition>,
884}
885
886#[derive(Debug, Clone, Serialize, Deserialize)]
888pub struct SetTablatureConfigCmd {
889 pub part_index: usize,
890 pub staff_index: usize,
891 pub config: Option<TablatureConfig>,
893}
894
895#[derive(Debug, Clone, Serialize, Deserialize)]
897pub struct SetStaffPresentationCmd {
898 pub part_index: usize,
899 pub staff_index: usize,
900 pub presentation: StaffPresentation,
901}
902
903#[derive(Debug, Clone, Serialize, Deserialize)]
905pub struct SetGuitarTechniqueCmd {
906 pub part_index: usize,
907 pub staff_index: usize,
908 pub measure_index: usize,
909 pub voice: usize,
910 pub note_index: usize,
911 pub technique: Option<GuitarTechnique>,
913}
914
915#[derive(Debug, Clone, Serialize, Deserialize)]
917pub struct SetGuitarBendAlterCmd {
918 pub part_index: usize,
919 pub staff_index: usize,
920 pub measure_index: usize,
921 pub voice: usize,
922 pub note_index: usize,
923 pub alter_cents: Option<i16>,
925}
926
927#[derive(Debug, Clone, Serialize, Deserialize)]
929pub struct SetGuitarBendCurveCmd {
930 pub part_index: usize,
931 pub staff_index: usize,
932 pub measure_index: usize,
933 pub voice: usize,
934 pub note_index: usize,
935 #[serde(default)]
936 pub points: Vec<crate::GuitarBendPoint>,
937}
938
939#[derive(Debug, Clone, Serialize, Deserialize)]
941pub struct SetExpressionTextCmd {
942 pub measure_index: usize,
943 pub text: Option<String>,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize)]
953pub struct SetMeasureTextCmd {
954 pub part_index: usize,
955 pub staff_index: usize,
956 pub measure_index: usize,
957 pub text_index: usize,
958 pub text: Option<StyledText>,
959}
960
961#[derive(Debug, Clone, Serialize, Deserialize)]
967pub struct SetScoreTextCmd {
968 pub text_index: usize,
969 pub text: Option<StyledText>,
970}
971
972#[derive(Debug, Clone, Serialize, Deserialize)]
976pub struct SetScoreStyleOverridesCmd {
977 pub overrides: Vec<ViewStyleOverride>,
978}
979
980#[derive(Debug, Clone, Serialize, Deserialize)]
982pub struct SetObjectStyleOverridesCmd {
983 pub overrides: Vec<ObjectStyleOverride>,
984}
985
986#[derive(Debug, Clone, Serialize, Deserialize)]
988pub struct SetCueCmd {
989 pub part_index: usize,
990 pub staff_index: usize,
991 pub measure_index: usize,
992 pub voice: usize,
993 pub note_index: usize,
994 pub is_cue: bool,
995}
996
997#[derive(Debug, Clone, Serialize, Deserialize)]
999pub struct SetUnpitchedCmd {
1000 pub part_index: usize,
1001 pub staff_index: usize,
1002 pub measure_index: usize,
1003 pub voice: usize,
1004 pub note_index: usize,
1005 pub is_unpitched: bool,
1006}
1007
1008#[derive(Debug, Clone, Serialize, Deserialize)]
1010pub struct SetInstrumentIdCmd {
1011 pub part_index: usize,
1012 pub staff_index: usize,
1013 pub measure_index: usize,
1014 pub voice: usize,
1015 pub note_index: usize,
1016 pub instrument_id: Option<String>,
1017}
1018
1019#[derive(Debug, Clone, Serialize, Deserialize)]
1021pub struct SetNotePlacementCmd {
1022 pub part_index: usize,
1023 pub staff_index: usize,
1024 pub measure_index: usize,
1025 pub voice: usize,
1026 pub note_index: usize,
1027 #[serde(default)]
1028 pub offset_x: Option<f64>,
1029 #[serde(default)]
1030 pub offset_y: Option<f64>,
1031 #[serde(default)]
1032 pub relative_x: Option<f64>,
1033 #[serde(default)]
1034 pub relative_y: Option<f64>,
1035}
1036
1037#[derive(Debug, Clone, Serialize, Deserialize)]
1039pub struct SetNoteHeadCmd {
1040 pub part_index: usize,
1041 pub staff_index: usize,
1042 pub measure_index: usize,
1043 pub voice: usize,
1044 pub note_index: usize,
1045 pub note_head: NoteHead,
1046}
1047
1048#[derive(Debug, Clone, Serialize, Deserialize)]
1050pub struct RespellScoreCmd {
1051 pub prefer_flat: bool,
1052}
1053
1054#[derive(Debug, Clone, Serialize, Deserialize)]
1056pub struct RespellScoreToKeyCmd {}
1057
1058struct UndoEntry {
1059 command: Command,
1060 snapshot: Score,
1061}
1062
1063pub struct CommandStack {
1064 history: Vec<UndoEntry>,
1065 future: Vec<(Command, Score)>,
1066 max_depth: usize,
1067}
1068
1069impl CommandStack {
1070 pub fn new(max_depth: usize) -> Self {
1071 Self {
1072 history: Vec::new(),
1073 future: Vec::new(),
1074 max_depth,
1075 }
1076 }
1077
1078 pub fn can_undo(&self) -> bool {
1079 !self.history.is_empty()
1080 }
1081
1082 pub fn can_redo(&self) -> bool {
1083 !self.future.is_empty()
1084 }
1085
1086 pub fn execute(&mut self, cmd: Command, score: &mut Score) -> Result<(), Error> {
1087 let snapshot = score.clone();
1088 let mut candidate = snapshot.clone();
1089 apply_command(&cmd, &mut candidate)?;
1090 if !validate(&candidate).is_valid() {
1091 return Err(Error::InvalidScore);
1092 }
1093 *score = candidate;
1094 self.history.push(UndoEntry {
1095 command: cmd,
1096 snapshot,
1097 });
1098 self.future.clear();
1099 if self.history.len() > self.max_depth {
1100 self.history.remove(0);
1101 }
1102 Ok(())
1103 }
1104
1105 pub fn undo(&mut self, score: &mut Score) -> Result<ChangeHint, Error> {
1106 let entry = self.history.last().ok_or(Error::NothingToUndo)?;
1107 if !validate(&entry.snapshot).is_valid() {
1108 return Err(Error::InvalidScore);
1109 }
1110 let entry = self.history.pop().ok_or(Error::NothingToUndo)?;
1111 let hint = command_hint(&entry.command);
1112 let post_snapshot = score.clone();
1113 *score = entry.snapshot;
1114 self.future.push((entry.command, post_snapshot));
1115 if self.future.len() > self.max_depth {
1116 self.future.remove(0);
1117 }
1118 Ok(hint)
1119 }
1120
1121 pub fn redo(&mut self, score: &mut Score) -> Result<ChangeHint, Error> {
1122 let (_, post) = self.future.last().ok_or(Error::NothingToRedo)?;
1123 if !validate(post).is_valid() {
1124 return Err(Error::InvalidScore);
1125 }
1126 let (cmd, post) = self.future.pop().ok_or(Error::NothingToRedo)?;
1127 let hint = command_hint(&cmd);
1128 let snapshot = score.clone();
1129 *score = post;
1130 self.history.push(UndoEntry {
1131 command: cmd,
1132 snapshot,
1133 });
1134 Ok(hint)
1135 }
1136
1137 pub fn history_commands(&self) -> Vec<Command> {
1140 self.history.iter().map(|e| e.command.clone()).collect()
1141 }
1142
1143 pub fn undo_label(&self) -> Option<String> {
1145 self.history.last().map(|e| command_label(&e.command))
1146 }
1147
1148 pub fn redo_label(&self) -> Option<String> {
1150 self.future.last().map(|(cmd, _)| command_label(cmd))
1151 }
1152
1153 pub fn undo_key(&self) -> Option<String> {
1155 self.history.last().map(|e| command_key(&e.command))
1156 }
1157
1158 pub fn redo_key(&self) -> Option<String> {
1160 self.future.last().map(|(cmd, _)| command_key(cmd))
1161 }
1162
1163 pub fn batch_execute(&mut self, cmds: Vec<Command>, score: &mut Score) -> Result<(), Error> {
1165 if cmds.is_empty() {
1166 return Ok(());
1167 }
1168 let snapshot = score.clone();
1169 let mut candidate = snapshot.clone();
1170 for cmd in &cmds {
1171 apply_command(cmd, &mut candidate)?;
1172 }
1173 if !validate(&candidate).is_valid() {
1174 return Err(Error::InvalidScore);
1175 }
1176 *score = candidate;
1177 self.history.push(UndoEntry {
1178 command: Command::Batch(BatchCmd {
1179 commands: cmds,
1180 label: None,
1181 }),
1182 snapshot,
1183 });
1184 self.future.clear();
1185 if self.history.len() > self.max_depth {
1186 self.history.remove(0);
1187 }
1188 Ok(())
1189 }
1190
1191 pub fn batch_execute_labeled(
1195 &mut self,
1196 cmds: Vec<Command>,
1197 label: String,
1198 score: &mut Score,
1199 ) -> Result<(), Error> {
1200 if cmds.is_empty() {
1201 return Ok(());
1202 }
1203 let snapshot = score.clone();
1204 let mut candidate = snapshot.clone();
1205 for cmd in &cmds {
1206 apply_command(cmd, &mut candidate)?;
1207 }
1208 if !validate(&candidate).is_valid() {
1209 return Err(Error::InvalidScore);
1210 }
1211 *score = candidate;
1212 self.history.push(UndoEntry {
1213 command: Command::Batch(BatchCmd {
1214 commands: cmds,
1215 label: Some(label),
1216 }),
1217 snapshot,
1218 });
1219 self.future.clear();
1220 if self.history.len() > self.max_depth {
1221 self.history.remove(0);
1222 }
1223 Ok(())
1224 }
1225}
1226
1227pub fn command_hint(cmd: &Command) -> ChangeHint {
1230 use ChangeScope::*;
1231 macro_rules! hint {
1232 ($scope:expr, $layout:expr, $playback:expr) => {
1233 ChangeHint {
1234 scope: $scope,
1235 layout_dirty: $layout,
1236 playback_dirty: $playback,
1237 }
1238 };
1239 }
1240 macro_rules! meas {
1241 ($c:expr) => {
1242 Measures {
1243 part: $c.part_index,
1244 staff: $c.staff_index,
1245 start: $c.measure_index,
1246 end: $c.measure_index + 1,
1247 }
1248 };
1249 }
1250 match cmd {
1251 Command::NewScore(_)
1253 | Command::AddPart(_)
1254 | Command::DeletePart(_)
1255 | Command::ReorderParts(_)
1256 | Command::AddMeasure(_)
1257 | Command::DeleteMeasure(_) => hint!(Global, true, true),
1258
1259 Command::SetTempo(_) => hint!(Global, false, true),
1260
1261 Command::SetMetadata(_) => hint!(Global, false, false),
1262
1263 Command::SetKeySignature(_) => hint!(Global, true, false),
1264
1265 Command::SetTimeSignature(_) => hint!(Global, true, true),
1266
1267 Command::SetBarline(_)
1268 | Command::SetVolta(_)
1269 | Command::SetRehearsalMark(_)
1270 | Command::SetNavigationMark(_)
1271 | Command::SetExpressionText(_) => hint!(Global, false, false),
1272 Command::SetMeasureText(c) => hint!(
1273 Measures {
1274 part: c.part_index,
1275 staff: c.staff_index,
1276 start: c.measure_index,
1277 end: c.measure_index + 1
1278 },
1279 false,
1280 false
1281 ),
1282 Command::SetScoreText(_)
1283 | Command::SetScoreStyleOverrides(_)
1284 | Command::SetObjectStyleOverrides(_) => hint!(Global, true, false),
1285
1286 Command::SetMultiRest(_) => hint!(Global, true, false),
1287
1288 Command::SetTempoAtMeasure(_) => hint!(Global, false, true),
1289 Command::SetTempoRampAtMeasure(_) => hint!(Global, false, true),
1290
1291 Command::SetPartName(c) => hint!(Part(c.part_index), false, false),
1293
1294 Command::SetMidiInstrument(c) => hint!(Part(c.part_index), false, true),
1295 Command::SetPercussionKit(c) => hint!(Part(c.part_index), true, true),
1296
1297 Command::SetInstrumentDefinition(c) => hint!(Part(c.part_index), true, false),
1298 Command::SetMeasureInstrumentChange(c) => hint!(meas!(c), true, true),
1299 Command::SetMeasureTablatureChange(c) => hint!(meas!(c), true, true),
1300
1301 Command::UpsertScoreView(_) | Command::RemoveScoreView(_) => hint!(Global, true, false),
1302
1303 Command::SetTranspose(c) => hint!(Part(c.part_index), false, true),
1304 Command::TransposeStaffRegion(c) => hint!(
1305 Measures {
1306 part: c.part_index,
1307 staff: c.staff_index,
1308 start: c.start_measure,
1309 end: c.end_measure
1310 },
1311 true,
1312 true
1313 ),
1314
1315 Command::SetClef(c) => hint!(Part(c.part_index), true, false),
1316
1317 Command::AddNote(c) => hint!(meas!(c), false, true),
1319 Command::AddPitch(c) => hint!(meas!(c), false, true),
1320 Command::SetDuration(c) => hint!(meas!(c), false, true),
1321 Command::DeleteNote(c) => hint!(meas!(c), false, true),
1322 Command::PasteVoice(c) => hint!(meas!(c), false, true),
1323 Command::PasteRange(c) => hint!(
1324 Measures {
1325 part: c.part_index,
1326 staff: c.staff_index,
1327 start: c.target_measure,
1328 end: c.target_measure + c.measures.len()
1329 },
1330 false,
1331 true
1332 ),
1333 Command::PasteScoreFragment(_) => hint!(Global, true, true),
1334 Command::ExchangeVoices(c) => hint!(
1335 Measures {
1336 part: c.part_index,
1337 staff: c.staff_index,
1338 start: c.start_measure.min(c.end_measure),
1339 end: c.start_measure.max(c.end_measure) + 1,
1340 },
1341 true,
1342 true
1343 ),
1344 Command::MoveOrCopyVoiceRange(_) => hint!(Global, true, true),
1345 Command::SplitMeasure(_) => hint!(Global, true, true),
1346 Command::JoinMeasures(_) => hint!(Global, true, true),
1347 Command::ImplodeStaves(_) | Command::ExplodeVoices(_) => hint!(Global, true, true),
1348 Command::ExplodeChordPitches(_) => hint!(Global, true, true),
1349 Command::ScaleVoiceRange(c) => hint!(
1350 Measures {
1351 part: c.part_index,
1352 staff: c.staff_index,
1353 start: c.start_measure.min(c.end_measure),
1354 end: c.start_measure.max(c.end_measure) + 1,
1355 },
1356 true,
1357 true
1358 ),
1359 Command::AddHairpin(c) => hint!(meas!(c), false, true),
1360 Command::ToggleTie(c) => hint!(meas!(c), false, true),
1361 Command::SetDynamic(c) => hint!(meas!(c), false, true),
1362 Command::ToggleArticulation(c) => hint!(meas!(c), false, true),
1363 Command::SetGrace(c) => hint!(meas!(c), false, true),
1364 Command::SetOttava(c) => hint!(meas!(c), false, true),
1365 Command::SetLyric(c) => hint!(meas!(c), false, true),
1366 Command::AddPedal(c) => hint!(meas!(c), false, true),
1367 Command::SetChordSymbol(c) => hint!(meas!(c), false, true),
1368 Command::SetHarmonyRange(c) => hint!(meas!(c), false, true),
1369 Command::SetFiguredBass(_) => hint!(Global, false, true),
1370 Command::SetHarpPedalDiagrams(c) => hint!(meas!(c), false, false),
1371
1372 Command::SetSystemBreak(_) | Command::SetPageBreak(_) | Command::SetSectionBreak(_) => {
1373 hint!(Global, true, false)
1374 }
1375
1376 Command::ToggleSlur(_) | Command::ToggleTrillLine(_) => hint!(Global, true, false),
1377 Command::SetGlissando(c) => hint!(meas!(c), true, true),
1378 Command::SetCrossStaff(c) => hint!(meas!(c), true, true),
1379 Command::AddSpanner(_) | Command::UpdateSpanner(_) | Command::RemoveSpanner(_) => {
1380 hint!(Global, true, true)
1381 }
1382
1383 Command::SetPartGroup(_) => hint!(Global, false, false),
1384
1385 Command::AddStaff(_) | Command::DeleteStaff(_) => hint!(Global, true, true),
1386
1387 Command::SetTuplet(c) => hint!(meas!(c), false, true),
1388
1389 Command::RespellScore(_) | Command::RespellScoreToKey(_) => hint!(Global, true, true),
1390
1391 Command::SetStem(c) => hint!(meas!(c), false, false),
1392
1393 Command::SetArpeggio(c) => hint!(meas!(c), false, false),
1394
1395 Command::SetTechniqueText(c) => hint!(meas!(c), false, false),
1396 Command::SetFingering(c) => hint!(meas!(c), false, false),
1397 Command::SetFingerings(c) => hint!(meas!(c), false, false),
1398 Command::SetStringNumber(c) => hint!(meas!(c), false, false),
1399 Command::SetTabPosition(c) => hint!(meas!(c), false, false),
1400 Command::SetTablatureConfig(c) => hint!(Part(c.part_index), true, true),
1401 Command::SetStaffPresentation(c) => hint!(Part(c.part_index), true, false),
1402 Command::SetGuitarTechnique(c) => hint!(meas!(c), false, false),
1403 Command::SetGuitarBendAlter(c) => hint!(meas!(c), false, false),
1404 Command::SetGuitarBendCurve(c) => hint!(meas!(c), false, false),
1405 Command::SetNoteHead(c) => hint!(meas!(c), false, false),
1406 Command::SetCue(c) => hint!(meas!(c), false, true),
1407 Command::SetUnpitched(c) => hint!(meas!(c), false, true),
1408 Command::SetInstrumentId(c) => hint!(meas!(c), false, true),
1409 Command::SetNotePlacement(c) => hint!(meas!(c), true, false),
1410
1411 Command::Batch(c) => {
1412 let Some(first) = c.commands.first() else {
1413 return hint!(Global, false, false);
1414 };
1415 let mut merged = command_hint(first);
1416 for cmd in c.commands.iter().skip(1) {
1417 merged = merged.merge(command_hint(cmd));
1418 }
1419 merged
1420 }
1421 }
1422}
1423
1424pub fn command_label(cmd: &Command) -> String {
1426 match cmd {
1427 Command::AddNote(_) => "Add Note".to_string(),
1428 Command::AddPitch(_) => "Add Pitch".to_string(),
1429 Command::SetDuration(_) => "Set Duration".to_string(),
1430 Command::DeleteNote(_) => "Delete Note".to_string(),
1431 Command::AddMeasure(_) => "Add Measure".to_string(),
1432 Command::DeleteMeasure(_) => "Delete Measure".to_string(),
1433 Command::SetTempo(_) => "Set Tempo".to_string(),
1434 Command::NewScore(_) => "New Score".to_string(),
1435 Command::AddHairpin(_) => "Add Hairpin".to_string(),
1436 Command::ToggleTie(_) => "Toggle Tie".to_string(),
1437 Command::SetDynamic(_) => "Set Dynamic".to_string(),
1438 Command::ToggleArticulation(_) => "Toggle Articulation".to_string(),
1439 Command::SetKeySignature(_) => "Set Key Signature".to_string(),
1440 Command::SetTimeSignature(_) => "Set Time Signature".to_string(),
1441 Command::SetBarline(_) => "Set Barline".to_string(),
1442 Command::AddPart(_) => "Add Part".to_string(),
1443 Command::DeletePart(_) => "Delete Part".to_string(),
1444 Command::ReorderParts(_) => "Reorder Parts".to_string(),
1445 Command::SetMetadata(_) => "Set Metadata".to_string(),
1446 Command::SetRehearsalMark(_) => "Set Rehearsal Mark".to_string(),
1447 Command::SetNavigationMark(_) => "Set Navigation Mark".to_string(),
1448 Command::SetChordSymbol(_) => "Set Chord Symbol".to_string(),
1449 Command::SetHarmonyRange(_) => "Set Harmony Range".to_string(),
1450 Command::SetHarpPedalDiagrams(_) => "Set Harp Pedal Diagrams".to_string(),
1451 Command::SetFiguredBass(_) => "Set Figured Bass".to_string(),
1452 Command::SetGrace(_) => "Set Grace Note".to_string(),
1453 Command::SetOttava(_) => "Set Ottava".to_string(),
1454 Command::SetLyric(_) => "Set Lyric".to_string(),
1455 Command::SetMultiRest(_) => "Set Multi-Rest".to_string(),
1456 Command::AddPedal(_) => "Add Pedal".to_string(),
1457 Command::SetVolta(_) => "Set Volta".to_string(),
1458 Command::SetClef(_) => "Set Clef".to_string(),
1459 Command::SetPartName(_) => "Set Part Name".to_string(),
1460 Command::SetMidiInstrument(_) => "Set MIDI Instrument".to_string(),
1461 Command::SetPercussionKit(_) => "Set Percussion Kit".to_string(),
1462 Command::SetInstrumentDefinition(_) => "Set Instrument Definition".to_string(),
1463 Command::SetMeasureInstrumentChange(_) => "Set Measure Instrument Change".to_string(),
1464 Command::SetMeasureTablatureChange(_) => "Set Measure Tablature Change".to_string(),
1465 Command::UpsertScoreView(_) => "Update Score View".to_string(),
1466 Command::RemoveScoreView(_) => "Remove Score View".to_string(),
1467 Command::SetTranspose(_) => "Set Transpose".to_string(),
1468 Command::TransposeStaffRegion(_) => "Transpose Staff Region".to_string(),
1469 Command::SetTempoAtMeasure(_) => "Set Tempo".to_string(),
1470 Command::SetTempoRampAtMeasure(_) => "Set Tempo Ramp".to_string(),
1471 Command::PasteVoice(_) => "Paste Voice".to_string(),
1472 Command::PasteRange(_) => "Paste Range".to_string(),
1473 Command::PasteScoreFragment(_) => "Paste Score Fragment".to_string(),
1474 Command::ExchangeVoices(_) => "Exchange Voices".to_string(),
1475 Command::MoveOrCopyVoiceRange(c) => if c.move_source {
1476 "Move Voice Range"
1477 } else {
1478 "Copy Voice Range"
1479 }
1480 .to_string(),
1481 Command::SplitMeasure(_) => "Split Measure".to_string(),
1482 Command::JoinMeasures(_) => "Join Measures".to_string(),
1483 Command::ImplodeStaves(_) => "Implode Staves".to_string(),
1484 Command::ExplodeVoices(_) => "Explode Voices".to_string(),
1485 Command::ExplodeChordPitches(_) => "Explode Chord Pitches".to_string(),
1486 Command::ScaleVoiceRange(c) => match c.scale {
1487 DurationScale::Half => "Halve Voice Durations",
1488 DurationScale::Double => "Double Voice Durations",
1489 }
1490 .to_string(),
1491 Command::SetSystemBreak(_) => "Set System Break".to_string(),
1492 Command::SetPageBreak(_) => "Set Page Break".to_string(),
1493 Command::SetSectionBreak(_) => "Set Section Break".to_string(),
1494 Command::ToggleSlur(_) => "Toggle Slur".to_string(),
1495 Command::AddStaff(_) => "Add Staff".to_string(),
1496 Command::DeleteStaff(_) => "Delete Staff".to_string(),
1497 Command::SetTuplet(c) => if c.tuplet.is_some() {
1498 "Set Tuplet"
1499 } else {
1500 "Clear Tuplet"
1501 }
1502 .to_string(),
1503 Command::SetInstrumentId(_) => "Set Note Instrument".to_string(),
1504 Command::SetNotePlacement(_) => "Set Note Placement".to_string(),
1505 Command::SetUnpitched(c) => if c.is_unpitched {
1506 "Set Unpitched Note"
1507 } else {
1508 "Clear Unpitched Note"
1509 }
1510 .to_string(),
1511 Command::RespellScore(c) => if c.prefer_flat {
1512 "Respell Score (flat)"
1513 } else {
1514 "Respell Score (sharp)"
1515 }
1516 .to_string(),
1517 Command::RespellScoreToKey(_) => "Respell Score to Key".to_string(),
1518 Command::SetStem(_) => "Set Stem".to_string(),
1519 Command::SetArpeggio(_) => "Set Arpeggio".to_string(),
1520 Command::SetTechniqueText(_) => "Set Technique Text".to_string(),
1521 Command::SetFingering(_) => "Set Fingering".to_string(),
1522 Command::SetFingerings(_) => "Set Fingering Candidates".to_string(),
1523 Command::SetStringNumber(_) => "Set String Number".to_string(),
1524 Command::SetTabPosition(_) => "Set Tablature Position".to_string(),
1525 Command::SetTablatureConfig(_) => "Set Tablature Configuration".to_string(),
1526 Command::SetStaffPresentation(_) => "Set Staff Presentation".to_string(),
1527 Command::SetGuitarTechnique(_) => "Set Guitar Technique".to_string(),
1528 Command::SetGuitarBendAlter(_) => "Set Guitar Bend Alter".to_string(),
1529 Command::SetGuitarBendCurve(_) => "Set Guitar Bend Curve".to_string(),
1530 Command::SetNoteHead(_) => "Set Note Head".to_string(),
1531 Command::SetCue(c) => if c.is_cue {
1532 "Set Cue Note"
1533 } else {
1534 "Clear Cue Note"
1535 }
1536 .to_string(),
1537 Command::SetExpressionText(_) => "Set Expression Text".to_string(),
1538 Command::SetMeasureText(c) => match c.text {
1539 Some(_) => "Set Measure Text",
1540 None => "Remove Measure Text",
1541 }
1542 .to_string(),
1543 Command::SetScoreText(c) => match c.text {
1544 Some(_) => "Set Score Text",
1545 None => "Remove Score Text",
1546 }
1547 .to_string(),
1548 Command::SetScoreStyleOverrides(_) => "Set Score Style Defaults".to_string(),
1549 Command::SetObjectStyleOverrides(_) => "Set Object Style Overrides".to_string(),
1550 Command::ToggleTrillLine(_) => "Toggle Trill Line".to_string(),
1551 Command::SetGlissando(_) => "Set Glissando".to_string(),
1552 Command::SetCrossStaff(_) => "Set Cross-Staff Placement".to_string(),
1553 Command::SetPartGroup(_) => "Set Part Group".to_string(),
1554 Command::AddSpanner(_) => "Add Notation Spanner".to_string(),
1555 Command::UpdateSpanner(_) => "Update Notation Spanner".to_string(),
1556 Command::RemoveSpanner(_) => "Remove Notation Spanner".to_string(),
1557 Command::Batch(c) => c.label.clone().unwrap_or_else(|| {
1558 c.commands
1559 .first()
1560 .map(command_label)
1561 .unwrap_or_else(|| "Batch".to_string())
1562 }),
1563 }
1564}
1565
1566pub fn command_key(cmd: &Command) -> String {
1570 match cmd {
1571 Command::AddNote(_) => "AddNote".to_string(),
1572 Command::AddPitch(_) => "AddPitch".to_string(),
1573 Command::SetDuration(_) => "SetDuration".to_string(),
1574 Command::DeleteNote(_) => "DeleteNote".to_string(),
1575 Command::AddMeasure(_) => "AddMeasure".to_string(),
1576 Command::DeleteMeasure(_) => "DeleteMeasure".to_string(),
1577 Command::SetTempo(_) => "SetTempo".to_string(),
1578 Command::NewScore(_) => "NewScore".to_string(),
1579 Command::AddHairpin(_) => "AddHairpin".to_string(),
1580 Command::ToggleTie(_) => "ToggleTie".to_string(),
1581 Command::SetDynamic(_) => "SetDynamic".to_string(),
1582 Command::ToggleArticulation(_) => "ToggleArticulation".to_string(),
1583 Command::SetKeySignature(_) => "SetKeySignature".to_string(),
1584 Command::SetTimeSignature(_) => "SetTimeSignature".to_string(),
1585 Command::SetBarline(_) => "SetBarline".to_string(),
1586 Command::AddPart(_) => "AddPart".to_string(),
1587 Command::DeletePart(_) => "DeletePart".to_string(),
1588 Command::ReorderParts(_) => "ReorderParts".to_string(),
1589 Command::SetMetadata(_) => "SetMetadata".to_string(),
1590 Command::SetRehearsalMark(_) => "SetRehearsalMark".to_string(),
1591 Command::SetNavigationMark(_) => "SetNavigationMark".to_string(),
1592 Command::SetChordSymbol(_) => "SetChordSymbol".to_string(),
1593 Command::SetHarmonyRange(_) => "SetHarmonyRange".to_string(),
1594 Command::SetFiguredBass(_) => "SetFiguredBass".to_string(),
1595 Command::SetHarpPedalDiagrams(_) => "SetHarpPedalDiagrams".to_string(),
1596 Command::SetGrace(_) => "SetGrace".to_string(),
1597 Command::SetOttava(_) => "SetOttava".to_string(),
1598 Command::SetLyric(_) => "SetLyric".to_string(),
1599 Command::SetMultiRest(_) => "SetMultiRest".to_string(),
1600 Command::AddPedal(_) => "AddPedal".to_string(),
1601 Command::SetVolta(_) => "SetVolta".to_string(),
1602 Command::SetClef(_) => "SetClef".to_string(),
1603 Command::SetPartName(_) => "SetPartName".to_string(),
1604 Command::SetMidiInstrument(_) => "SetMidiInstrument".to_string(),
1605 Command::SetPercussionKit(_) => "SetPercussionKit".to_string(),
1606 Command::SetInstrumentDefinition(_) => "SetInstrumentDefinition".to_string(),
1607 Command::SetMeasureInstrumentChange(_) => "SetMeasureInstrumentChange".to_string(),
1608 Command::SetMeasureTablatureChange(_) => "SetMeasureTablatureChange".to_string(),
1609 Command::UpsertScoreView(_) => "UpsertScoreView".to_string(),
1610 Command::RemoveScoreView(_) => "RemoveScoreView".to_string(),
1611 Command::SetTranspose(_) => "SetTranspose".to_string(),
1612 Command::TransposeStaffRegion(_) => "TransposeStaffRegion".to_string(),
1613 Command::SetTempoAtMeasure(_) => "SetTempoAtMeasure".to_string(),
1614 Command::SetTempoRampAtMeasure(_) => "SetTempoRampAtMeasure".to_string(),
1615 Command::PasteVoice(_) => "PasteVoice".to_string(),
1616 Command::PasteRange(_) => "PasteRange".to_string(),
1617 Command::PasteScoreFragment(_) => "PasteScoreFragment".to_string(),
1618 Command::ExchangeVoices(_) => "ExchangeVoices".to_string(),
1619 Command::MoveOrCopyVoiceRange(_) => "MoveOrCopyVoiceRange".to_string(),
1620 Command::SplitMeasure(_) => "SplitMeasure".to_string(),
1621 Command::JoinMeasures(_) => "JoinMeasures".to_string(),
1622 Command::ImplodeStaves(_) => "ImplodeStaves".to_string(),
1623 Command::ExplodeVoices(_) => "ExplodeVoices".to_string(),
1624 Command::ExplodeChordPitches(_) => "ExplodeChordPitches".to_string(),
1625 Command::ScaleVoiceRange(_) => "ScaleVoiceRange".to_string(),
1626 Command::SetSystemBreak(_) => "SetSystemBreak".to_string(),
1627 Command::SetPageBreak(_) => "SetPageBreak".to_string(),
1628 Command::SetSectionBreak(_) => "SetSectionBreak".to_string(),
1629 Command::ToggleSlur(_) => "ToggleSlur".to_string(),
1630 Command::AddStaff(_) => "AddStaff".to_string(),
1631 Command::DeleteStaff(_) => "DeleteStaff".to_string(),
1632 Command::SetTuplet(_) => "SetTuplet".to_string(),
1633 Command::RespellScore(_) => "RespellScore".to_string(),
1634 Command::RespellScoreToKey(_) => "RespellScoreToKey".to_string(),
1635 Command::SetStem(_) => "SetStem".to_string(),
1636 Command::SetArpeggio(_) => "SetArpeggio".to_string(),
1637 Command::SetTechniqueText(_) => "SetTechniqueText".to_string(),
1638 Command::SetFingering(_) => "SetFingering".to_string(),
1639 Command::SetFingerings(_) => "SetFingerings".to_string(),
1640 Command::SetStringNumber(_) => "SetStringNumber".to_string(),
1641 Command::SetTabPosition(_) => "SetTabPosition".to_string(),
1642 Command::SetTablatureConfig(_) => "SetTablatureConfig".to_string(),
1643 Command::SetStaffPresentation(_) => "SetStaffPresentation".to_string(),
1644 Command::SetGuitarTechnique(_) => "SetGuitarTechnique".to_string(),
1645 Command::SetGuitarBendAlter(_) => "SetGuitarBendAlter".to_string(),
1646 Command::SetGuitarBendCurve(_) => "SetGuitarBendCurve".to_string(),
1647 Command::SetNoteHead(_) => "SetNoteHead".to_string(),
1648 Command::SetCue(_) => "SetCue".to_string(),
1649 Command::SetUnpitched(_) => "SetUnpitched".to_string(),
1650 Command::SetInstrumentId(_) => "SetInstrumentId".to_string(),
1651 Command::SetNotePlacement(_) => "SetNotePlacement".to_string(),
1652 Command::SetExpressionText(_) => "SetExpressionText".to_string(),
1653 Command::SetMeasureText(_) => "SetMeasureText".to_string(),
1654 Command::SetScoreText(_) => "SetScoreText".to_string(),
1655 Command::SetScoreStyleOverrides(_) => "SetScoreStyleOverrides".to_string(),
1656 Command::SetObjectStyleOverrides(_) => "SetObjectStyleOverrides".to_string(),
1657 Command::ToggleTrillLine(_) => "ToggleTrillLine".to_string(),
1658 Command::SetGlissando(_) => "SetGlissando".to_string(),
1659 Command::SetCrossStaff(_) => "SetCrossStaff".to_string(),
1660 Command::SetPartGroup(_) => "SetPartGroup".to_string(),
1661 Command::AddSpanner(_) => "AddSpanner".to_string(),
1662 Command::UpdateSpanner(_) => "UpdateSpanner".to_string(),
1663 Command::RemoveSpanner(_) => "RemoveSpanner".to_string(),
1664 Command::Batch(c) => c.label.clone().unwrap_or_else(|| "Batch".to_string()),
1665 }
1666}
1667
1668pub fn apply_command(cmd: &Command, score: &mut Score) -> Result<(), Error> {
1669 match cmd {
1670 Command::AddNote(c) => apply_add_note(c, score),
1671 Command::AddPitch(c) => apply_add_pitch(c, score),
1672 Command::SetDuration(c) => apply_set_duration(c, score),
1673 Command::DeleteNote(c) => apply_delete_note(c, score),
1674 Command::AddMeasure(c) => apply_add_measure(c, score),
1675 Command::DeleteMeasure(c) => apply_delete_measure(c, score),
1676 Command::SetTempo(c) => {
1677 score.settings.tempo_bpm = c.bpm;
1678 Ok(())
1679 }
1680 Command::NewScore(c) => {
1681 let mut s = match c.template {
1682 Some(kind) => Score::template(kind),
1683 None => Score::new(
1684 &c.title,
1685 c.tempo_bpm,
1686 c.time_numerator,
1687 c.time_denominator,
1688 c.key_fifths,
1689 c.measure_count,
1690 ),
1691 };
1692 if c.template.is_some() {
1693 s.metadata.title = c.title.clone();
1694 s.metadata.composer = c.composer.clone();
1695 s.settings.tempo_bpm = c.tempo_bpm;
1696 s.settings.time_signature = TimeSignature {
1697 numerator: c.time_numerator,
1698 denominator: c.time_denominator,
1699 };
1700 s.settings.key_signature = KeySignature {
1701 fifths: c.key_fifths,
1702 mode: "major".to_string(),
1703 };
1704 for part in &mut s.parts {
1705 for staff in &mut part.staves {
1706 staff.measures.clear();
1707 for i in 0..c.measure_count {
1708 let mut m = Measure::empty(c.time_numerator, c.time_denominator);
1709 m.number = i + 1;
1710 staff.measures.push(m);
1711 }
1712 }
1713 }
1714 }
1715 *score = s;
1716 Ok(())
1717 }
1718 Command::AddHairpin(c) => apply_add_hairpin(c, score),
1719 Command::ToggleTie(c) => apply_toggle_tie(c, score),
1720 Command::SetDynamic(c) => apply_set_dynamic(c, score),
1721 Command::ToggleArticulation(c) => apply_toggle_articulation(c, score),
1722 Command::SetKeySignature(c) => {
1723 score.settings.key_signature = KeySignature {
1724 fifths: c.fifths,
1725 mode: "major".to_string(),
1726 };
1727 Ok(())
1728 }
1729 Command::SetTimeSignature(c) => apply_set_time_signature(c, score),
1730 Command::SetBarline(c) => apply_set_barline(c, score),
1731 Command::AddPart(c) => apply_add_part(c, score),
1732 Command::DeletePart(c) => apply_delete_part(c, score),
1733 Command::ReorderParts(c) => apply_reorder_parts(c, score),
1734 Command::SetMetadata(c) => apply_set_metadata(c, score),
1735 Command::SetRehearsalMark(c) => {
1736 for_each_measure_at(score, c.measure_index, |m| {
1737 m.rehearsal = c.text.clone();
1738 });
1739 Ok(())
1740 }
1741 Command::SetNavigationMark(c) => {
1742 for_each_measure_at(score, c.measure_index, |m| {
1743 m.navigation = c.mark.clone();
1744 });
1745 Ok(())
1746 }
1747 Command::SetChordSymbol(c) => {
1748 get_note_mut(
1749 score,
1750 c.part_index,
1751 c.staff_index,
1752 c.measure_index,
1753 c.voice,
1754 c.note_index,
1755 )?
1756 .chord_symbol = c.chord.clone();
1757 Ok(())
1758 }
1759 Command::SetHarmonyRange(c) => {
1760 if let Some(end) = &c.end
1761 && !score
1762 .parts
1763 .get(end.part)
1764 .and_then(|part| part.staves.get(end.staff))
1765 .and_then(|staff| staff.measures.get(end.measure))
1766 .and_then(|measure| measure.voices.get(end.voice))
1767 .and_then(|voice| voice.get(end.note))
1768 .is_some()
1769 {
1770 return Err(Error::InvalidCommand(
1771 "harmony range end does not point to an existing note".into(),
1772 ));
1773 }
1774 let note = get_note_mut(
1775 score,
1776 c.part_index,
1777 c.staff_index,
1778 c.measure_index,
1779 c.voice,
1780 c.note_index,
1781 )?;
1782 let chord = note.chord_symbol.as_mut().ok_or_else(|| {
1783 Error::InvalidCommand("cannot set a harmony range without a chord symbol".into())
1784 })?;
1785 chord.range_end = c.end.clone();
1786 Ok(())
1787 }
1788 Command::SetFiguredBass(c) => {
1789 for part in &mut score.parts {
1790 for staff in &mut part.staves {
1791 if let Some(measure) = staff.measures.get_mut(c.measure_index) {
1792 measure.figured_bass = c.figures.clone();
1793 }
1794 }
1795 }
1796 Ok(())
1797 }
1798 Command::SetHarpPedalDiagrams(c) => {
1799 let measure = score
1800 .parts
1801 .get_mut(c.part_index)
1802 .ok_or(Error::PartNotFound(c.part_index))?
1803 .staves
1804 .get_mut(c.staff_index)
1805 .ok_or(Error::StaffNotFound(c.staff_index))?
1806 .measures
1807 .get_mut(c.measure_index)
1808 .ok_or(Error::MeasureNotFound(c.measure_index))?;
1809 measure.harp_pedal_diagrams = c.diagrams.clone();
1810 Ok(())
1811 }
1812 Command::SetGrace(c) => {
1813 let note = get_note_mut(
1814 score,
1815 c.part_index,
1816 c.staff_index,
1817 c.measure_index,
1818 c.voice,
1819 c.note_index,
1820 )?;
1821 if note.is_rest {
1822 return Err(Error::InvalidCommand(
1823 "cannot make a rest into a grace note".into(),
1824 ));
1825 }
1826 note.is_grace = c.is_grace;
1827 note.grace_slash = c.slash;
1828 Ok(())
1829 }
1830 Command::SetOttava(c) => {
1831 let note = get_note_mut(
1832 score,
1833 c.part_index,
1834 c.staff_index,
1835 c.measure_index,
1836 c.voice,
1837 c.note_index,
1838 )?;
1839 note.ottava_start = c.ottava_start;
1840 note.ottava_end = c.ottava_end;
1841 Ok(())
1842 }
1843 Command::SetLyric(c) => {
1844 get_note_mut(
1845 score,
1846 c.part_index,
1847 c.staff_index,
1848 c.measure_index,
1849 c.voice,
1850 c.note_index,
1851 )?
1852 .lyric = c.lyric.clone();
1853 Ok(())
1854 }
1855 Command::SetMultiRest(c) => {
1856 for_each_measure_at(score, c.measure_index, |m| {
1857 m.multi_rest_count = c.count;
1858 });
1859 Ok(())
1860 }
1861 Command::AddPedal(c) => apply_add_pedal(c, score),
1862 Command::SetVolta(c) => {
1863 for_each_measure_at(score, c.measure_index, |m| {
1864 m.volta = c.volta.clone();
1865 });
1866 Ok(())
1867 }
1868 Command::SetClef(c) => apply_set_clef(c, score),
1869 Command::SetPartName(c) => apply_set_part_name(c, score),
1870 Command::SetMidiInstrument(c) => apply_set_midi_instrument(c, score),
1871 Command::SetPercussionKit(c) => apply_set_percussion_kit(c, score),
1872 Command::SetInstrumentDefinition(c) => apply_set_instrument_definition(c, score),
1873 Command::SetMeasureInstrumentChange(c) => apply_set_measure_instrument_change(c, score),
1874 Command::SetMeasureTablatureChange(c) => apply_set_measure_tablature_change(c, score),
1875 Command::UpsertScoreView(c) => apply_upsert_score_view(c, score),
1876 Command::RemoveScoreView(c) => apply_remove_score_view(c, score),
1877 Command::SetTranspose(c) => apply_set_transpose(c, score),
1878 Command::TransposeStaffRegion(c) => apply_transpose_staff_region(c, score),
1879 Command::SetTempoAtMeasure(c) => {
1880 for_each_measure_at(score, c.measure_index, |m| {
1881 m.tempo = c.bpm;
1882 });
1883 Ok(())
1884 }
1885 Command::SetTempoRampAtMeasure(c) => {
1886 for_each_measure_at(score, c.measure_index, |m| {
1887 m.tempo_ramp_to = c.target_bpm;
1888 });
1889 Ok(())
1890 }
1891 Command::PasteVoice(c) => apply_paste_voice(c, score),
1892 Command::PasteRange(c) => apply_paste_range(c, score),
1893 Command::PasteScoreFragment(c) => apply_paste_score_fragment(c, score),
1894 Command::ExchangeVoices(c) => apply_exchange_voices(c, score),
1895 Command::MoveOrCopyVoiceRange(c) => apply_move_or_copy_voice_range(c, score),
1896 Command::SplitMeasure(c) => apply_split_measure(c, score),
1897 Command::JoinMeasures(c) => apply_join_measures(c, score),
1898 Command::ImplodeStaves(c) => apply_implode_staves(c, score),
1899 Command::ExplodeVoices(c) => apply_explode_voices(c, score),
1900 Command::ExplodeChordPitches(c) => apply_explode_chord_pitches(c, score),
1901 Command::ScaleVoiceRange(c) => apply_scale_voice_range(c, score),
1902 Command::SetSystemBreak(c) => {
1903 for_each_measure_at(score, c.measure_index, |m| {
1904 m.system_break = c.value;
1905 });
1906 Ok(())
1907 }
1908 Command::SetPageBreak(c) => {
1909 for_each_measure_at(score, c.measure_index, |m| {
1910 m.page_break = c.value;
1911 });
1912 Ok(())
1913 }
1914 Command::SetSectionBreak(c) => {
1915 if c.measure_index >= score.measure_count() {
1916 return Err(Error::MeasureNotFound(c.measure_index));
1917 }
1918 for_each_measure_at(score, c.measure_index, |m| {
1919 m.section_break = c.value;
1920 });
1921 Ok(())
1922 }
1923 Command::ToggleSlur(c) => apply_toggle_slur(c, score),
1924 Command::AddStaff(c) => apply_add_staff(c, score),
1925 Command::DeleteStaff(c) => apply_delete_staff(c, score),
1926 Command::SetTuplet(c) => {
1927 get_note_mut(
1928 score,
1929 c.part_index,
1930 c.staff_index,
1931 c.measure_index,
1932 c.voice_index,
1933 c.note_index,
1934 )?
1935 .tuplet = c.tuplet.clone();
1936 Ok(())
1937 }
1938 Command::RespellScore(c) => {
1939 respell_score(score, c.prefer_flat);
1940 Ok(())
1941 }
1942 Command::RespellScoreToKey(_) => {
1943 respell_score_to_key(score);
1944 Ok(())
1945 }
1946 Command::SetStem(c) => {
1947 get_note_mut(
1948 score,
1949 c.part_index,
1950 c.staff_index,
1951 c.measure_index,
1952 c.voice_index,
1953 c.note_index,
1954 )?
1955 .stem_up = c.stem_up;
1956 Ok(())
1957 }
1958 Command::SetArpeggio(c) => {
1959 get_note_mut(
1960 score,
1961 c.part_index,
1962 c.staff_index,
1963 c.measure_index,
1964 c.voice_index,
1965 c.note_index,
1966 )?
1967 .arpeggiate = c.direction;
1968 Ok(())
1969 }
1970 Command::SetTechniqueText(c) => {
1971 get_note_mut(
1972 score,
1973 c.part_index,
1974 c.staff_index,
1975 c.measure_index,
1976 c.voice,
1977 c.note_index,
1978 )?
1979 .technique_text = c.text.clone();
1980 Ok(())
1981 }
1982 Command::SetGlissando(c) => {
1983 let note = get_note_mut(
1984 score,
1985 c.part_index,
1986 c.staff_index,
1987 c.measure_index,
1988 c.voice,
1989 c.note_index,
1990 )?;
1991 note.glissando_start = c.start;
1992 note.glissando_end = c.end;
1993 Ok(())
1994 }
1995 Command::SetCrossStaff(c) => {
1996 let staff_count = score
1997 .parts
1998 .get(c.part_index)
1999 .ok_or(Error::PartNotFound(c.part_index))?
2000 .staves
2001 .len();
2002 let note = get_note_mut(
2003 score,
2004 c.part_index,
2005 c.staff_index,
2006 c.measure_index,
2007 c.voice,
2008 c.note_index,
2009 )?;
2010 if let Some(ref placement) = c.placement
2011 && placement.target_staff == c.staff_index
2012 {
2013 return Err(Error::InvalidCommand(
2014 "cross-staff target must differ from source staff".into(),
2015 ));
2016 }
2017 if let Some(ref placement) = c.placement
2018 && placement.target_staff >= staff_count
2019 {
2020 return Err(Error::StaffNotFound(placement.target_staff));
2021 }
2022 note.cross_staff = c.placement.clone();
2023 Ok(())
2024 }
2025 Command::SetFingering(c) => {
2026 let note = get_note_mut(
2027 score,
2028 c.part_index,
2029 c.staff_index,
2030 c.measure_index,
2031 c.voice,
2032 c.note_index,
2033 )?;
2034 note.fingering = c.fingering;
2035 note.fingerings = c.fingering.into_iter().collect();
2036 Ok(())
2037 }
2038 Command::SetFingerings(c) => {
2039 let note = get_note_mut(
2040 score,
2041 c.part_index,
2042 c.staff_index,
2043 c.measure_index,
2044 c.voice,
2045 c.note_index,
2046 )?;
2047 note.fingerings = c.fingerings.clone();
2048 note.fingering = note.fingerings.first().copied();
2049 Ok(())
2050 }
2051 Command::SetStringNumber(c) => {
2052 get_note_mut(
2053 score,
2054 c.part_index,
2055 c.staff_index,
2056 c.measure_index,
2057 c.voice,
2058 c.note_index,
2059 )?
2060 .string_number = c.string_number;
2061 Ok(())
2062 }
2063 Command::SetTabPosition(c) => {
2064 let note = get_note_mut(
2065 score,
2066 c.part_index,
2067 c.staff_index,
2068 c.measure_index,
2069 c.voice,
2070 c.note_index,
2071 )?;
2072 note.string_number = c.position.as_ref().map(|position| position.string);
2073 note.tab_position = c.position.clone();
2074 note.tab_positions = c.position.iter().cloned().collect();
2075 Ok(())
2076 }
2077 Command::SetGuitarTechnique(c) => {
2078 get_note_mut(
2079 score,
2080 c.part_index,
2081 c.staff_index,
2082 c.measure_index,
2083 c.voice,
2084 c.note_index,
2085 )?
2086 .guitar_technique = c.technique.clone();
2087 Ok(())
2088 }
2089 Command::SetGuitarBendAlter(c) => {
2090 get_note_mut(
2091 score,
2092 c.part_index,
2093 c.staff_index,
2094 c.measure_index,
2095 c.voice,
2096 c.note_index,
2097 )?
2098 .guitar_bend_alter_cents = c.alter_cents;
2099 Ok(())
2100 }
2101 Command::SetGuitarBendCurve(c) => {
2102 get_note_mut(
2103 score,
2104 c.part_index,
2105 c.staff_index,
2106 c.measure_index,
2107 c.voice,
2108 c.note_index,
2109 )?
2110 .guitar_bend_curve = c.points.clone();
2111 Ok(())
2112 }
2113 Command::SetNoteHead(c) => {
2114 get_note_mut(
2115 score,
2116 c.part_index,
2117 c.staff_index,
2118 c.measure_index,
2119 c.voice,
2120 c.note_index,
2121 )?
2122 .note_head = c.note_head.clone();
2123 Ok(())
2124 }
2125 Command::SetCue(c) => {
2126 get_note_mut(
2127 score,
2128 c.part_index,
2129 c.staff_index,
2130 c.measure_index,
2131 c.voice,
2132 c.note_index,
2133 )?
2134 .is_cue = c.is_cue;
2135 Ok(())
2136 }
2137 Command::SetUnpitched(c) => {
2138 get_note_mut(
2139 score,
2140 c.part_index,
2141 c.staff_index,
2142 c.measure_index,
2143 c.voice,
2144 c.note_index,
2145 )?
2146 .is_unpitched = c.is_unpitched;
2147 Ok(())
2148 }
2149 Command::SetInstrumentId(c) => {
2150 get_note_mut(
2151 score,
2152 c.part_index,
2153 c.staff_index,
2154 c.measure_index,
2155 c.voice,
2156 c.note_index,
2157 )?
2158 .instrument_id = c.instrument_id.clone();
2159 Ok(())
2160 }
2161 Command::SetNotePlacement(c) => {
2162 let note = get_note_mut(
2163 score,
2164 c.part_index,
2165 c.staff_index,
2166 c.measure_index,
2167 c.voice,
2168 c.note_index,
2169 )?;
2170 for value in [c.offset_x, c.offset_y, c.relative_x, c.relative_y] {
2171 if value.is_some_and(|value| !value.is_finite()) {
2172 return Err(Error::InvalidCommand(
2173 "note placement offsets must be finite".into(),
2174 ));
2175 }
2176 }
2177 note.offset_x = c.offset_x;
2178 note.offset_y = c.offset_y;
2179 note.relative_x = c.relative_x;
2180 note.relative_y = c.relative_y;
2181 Ok(())
2182 }
2183 Command::SetExpressionText(c) => {
2184 for_each_measure_at(score, c.measure_index, |m| {
2185 m.expression_text = c.text.clone();
2186 });
2187 Ok(())
2188 }
2189 Command::SetMeasureText(c) => apply_set_measure_text(c, score),
2190 Command::SetScoreText(c) => apply_set_score_text(c, score),
2191 Command::SetScoreStyleOverrides(c) => apply_set_score_style_overrides(c, score),
2192 Command::SetObjectStyleOverrides(c) => apply_set_object_style_overrides(c, score),
2193 Command::SetTablatureConfig(c) => apply_set_tablature_config(c, score),
2194 Command::SetStaffPresentation(c) => apply_set_staff_presentation(c, score),
2195 Command::ToggleTrillLine(c) => apply_toggle_trill_line(c, score),
2196 Command::SetPartGroup(c) => {
2197 if let Some(group) = &c.group {
2198 score
2199 .part_groups
2200 .retain(|g| g.first_part != group.first_part || g.last_part != group.last_part);
2201 score.part_groups.push(group.clone());
2202 } else {
2203 score.part_groups.clear();
2205 }
2206 Ok(())
2207 }
2208 Command::AddSpanner(c) => {
2209 if score
2210 .spanners
2211 .iter()
2212 .any(|spanner| spanner.id == c.spanner.id)
2213 {
2214 return Err(Error::InvalidCommand(format!(
2215 "notation spanner id already exists: {}",
2216 c.spanner.id
2217 )));
2218 }
2219 score.spanners.push(c.spanner.clone());
2220 Ok(())
2221 }
2222 Command::UpdateSpanner(c) => {
2223 let index = score
2224 .spanners
2225 .iter()
2226 .position(|spanner| spanner.id == c.spanner.id)
2227 .ok_or_else(|| {
2228 Error::InvalidCommand(format!(
2229 "notation spanner id does not exist: {}",
2230 c.spanner.id
2231 ))
2232 })?;
2233 let previous = score.spanners[index].clone();
2234 score.spanners[index] = c.spanner.clone();
2235 clear_legacy_spanner_endpoints(score, &previous);
2236 clear_legacy_spanner_endpoints(score, &c.spanner);
2237 Ok(())
2238 }
2239 Command::RemoveSpanner(c) => {
2240 let index = score
2241 .spanners
2242 .iter()
2243 .position(|spanner| spanner.id == c.id)
2244 .ok_or_else(|| {
2245 Error::InvalidCommand(format!("notation spanner id does not exist: {}", c.id))
2246 })?;
2247 let removed = score.spanners.remove(index);
2248 clear_legacy_spanner_endpoints(score, &removed);
2249 Ok(())
2250 }
2251 Command::Batch(c) => {
2252 for cmd in &c.commands {
2253 apply_command(cmd, score)?;
2254 }
2255 Ok(())
2256 }
2257 }
2258}
2259
2260fn apply_set_tablature_config(cmd: &SetTablatureConfigCmd, score: &mut Score) -> Result<(), Error> {
2263 let staff = score
2264 .parts
2265 .get_mut(cmd.part_index)
2266 .ok_or(Error::PartNotFound(cmd.part_index))?
2267 .staves
2268 .get_mut(cmd.staff_index)
2269 .ok_or_else(|| Error::InvalidCommand(format!("staff {} out of range", cmd.staff_index)))?;
2270 staff.tablature = cmd.config.clone();
2271 if staff.tablature.is_some() {
2272 staff.presentation.kind = StaffKind::Tablature;
2273 } else if staff.presentation.kind == StaffKind::Tablature {
2274 staff.presentation.kind = StaffKind::Standard;
2275 }
2276 if staff.tablature.is_none() {
2277 for measure in &mut staff.measures {
2278 measure.tablature_change = None;
2279 }
2280 }
2281 Ok(())
2282}
2283
2284fn apply_set_staff_presentation(
2285 cmd: &SetStaffPresentationCmd,
2286 score: &mut Score,
2287) -> Result<(), Error> {
2288 if !(1..=64).contains(&cmd.presentation.lines) {
2289 return Err(Error::InvalidCommand(format!(
2290 "staff line count {} is outside 1..=64",
2291 cmd.presentation.lines
2292 )));
2293 }
2294 if !cmd.presentation.line_distance.is_finite()
2295 || !(0.1..=16.0).contains(&cmd.presentation.line_distance)
2296 {
2297 return Err(Error::InvalidCommand(
2298 "staff line distance must be finite and within 0.1..=16.0".into(),
2299 ));
2300 }
2301 let staff = score
2302 .parts
2303 .get_mut(cmd.part_index)
2304 .ok_or(Error::PartNotFound(cmd.part_index))?
2305 .staves
2306 .get_mut(cmd.staff_index)
2307 .ok_or_else(|| Error::InvalidCommand(format!("staff {} out of range", cmd.staff_index)))?;
2308 if cmd.presentation.kind == StaffKind::Tablature && staff.tablature.is_none() {
2309 return Err(Error::InvalidCommand(
2310 "tablature staff presentation requires a tablature configuration".into(),
2311 ));
2312 }
2313 staff.presentation = cmd.presentation.clone();
2314 Ok(())
2315}
2316
2317fn get_note_mut(
2318 score: &mut Score,
2319 part_index: usize,
2320 staff_index: usize,
2321 measure_index: usize,
2322 voice: usize,
2323 note_index: usize,
2324) -> Result<&mut Note, Error> {
2325 score
2326 .parts
2327 .get_mut(part_index)
2328 .ok_or(Error::PartNotFound(part_index))?
2329 .staves
2330 .get_mut(staff_index)
2331 .ok_or(Error::StaffNotFound(staff_index))?
2332 .measures
2333 .get_mut(measure_index)
2334 .ok_or(Error::MeasureNotFound(measure_index))?
2335 .voices
2336 .get_mut(voice)
2337 .ok_or(Error::VoiceOutOfRange(voice))?
2338 .get_mut(note_index)
2339 .ok_or(Error::NoteNotFound(note_index))
2340}
2341
2342fn for_each_measure_at(score: &mut Score, index: usize, mut f: impl FnMut(&mut Measure)) {
2343 for part in &mut score.parts {
2344 for staff in &mut part.staves {
2345 if let Some(m) = staff.measures.get_mut(index) {
2346 f(m);
2347 }
2348 }
2349 }
2350}
2351
2352fn apply_set_measure_text(cmd: &SetMeasureTextCmd, score: &mut Score) -> Result<(), Error> {
2353 let measure = score
2354 .parts
2355 .get_mut(cmd.part_index)
2356 .ok_or(Error::PartNotFound(cmd.part_index))?
2357 .staves
2358 .get_mut(cmd.staff_index)
2359 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2360 .measures
2361 .get_mut(cmd.measure_index)
2362 .ok_or(Error::MeasureNotFound(cmd.measure_index))?;
2363 if cmd.text_index > measure.texts.len()
2364 || (cmd.text.is_none() && cmd.text_index == measure.texts.len())
2365 {
2366 return Err(Error::InvalidCommand(format!(
2367 "styled text index {} out of range for {} entries",
2368 cmd.text_index,
2369 measure.texts.len()
2370 )));
2371 }
2372 if let Some(text) = &cmd.text {
2373 if cmd.text_index == measure.texts.len() {
2374 measure.texts.push(text.clone());
2375 } else {
2376 measure.texts[cmd.text_index] = text.clone();
2377 }
2378 } else {
2379 measure.texts.remove(cmd.text_index);
2380 }
2381 Ok(())
2382}
2383
2384fn apply_set_score_text(cmd: &SetScoreTextCmd, score: &mut Score) -> Result<(), Error> {
2385 if cmd.text_index > score.texts.len()
2386 || (cmd.text.is_none() && cmd.text_index == score.texts.len())
2387 {
2388 return Err(Error::InvalidCommand(format!(
2389 "styled score text index {} out of range for {} entries",
2390 cmd.text_index,
2391 score.texts.len()
2392 )));
2393 }
2394 if let Some(text) = &cmd.text {
2395 if cmd.text_index == score.texts.len() {
2396 score.texts.push(text.clone());
2397 } else {
2398 score.texts[cmd.text_index] = text.clone();
2399 }
2400 } else {
2401 score.texts.remove(cmd.text_index);
2402 }
2403 Ok(())
2404}
2405
2406fn apply_set_score_style_overrides(
2407 cmd: &SetScoreStyleOverridesCmd,
2408 score: &mut Score,
2409) -> Result<(), Error> {
2410 if cmd
2411 .overrides
2412 .iter()
2413 .any(|override_| !override_.value.is_finite() || !(0.05..=64.0).contains(&override_.value))
2414 {
2415 return Err(Error::InvalidCommand(
2416 "score typed style override values must be finite and within 0.05..=64".into(),
2417 ));
2418 }
2419 score.style_overrides = cmd.overrides.clone();
2420 Ok(())
2421}
2422
2423fn apply_set_object_style_overrides(
2424 cmd: &SetObjectStyleOverridesCmd,
2425 score: &mut Score,
2426) -> Result<(), Error> {
2427 let mut candidate = score.clone();
2428 candidate.object_style_overrides = cmd.overrides.clone();
2429 if validate(&candidate).errors.iter().any(|error| {
2430 matches!(
2431 error,
2432 super::validate::ValidationError::InvalidObjectStyleOverride { .. }
2433 )
2434 }) {
2435 return Err(Error::InvalidCommand(
2436 "object style overrides require existing targets, finite values within 0.05..=64, and bounded provenance".into(),
2437 ));
2438 }
2439 score.object_style_overrides = cmd.overrides.clone();
2440 Ok(())
2441}
2442
2443fn apply_add_note(cmd: &AddNoteCmd, score: &mut Score) -> Result<(), Error> {
2444 let ts_beats = score.settings.time_signature.total_beats();
2445 let note = if cmd.is_rest {
2446 let mut n = Note::rest(cmd.duration.clone());
2447 n.dot_count = cmd.dot_count;
2448 n.tuplet = cmd.tuplet.clone();
2449 n
2450 } else {
2451 let pitch = cmd
2452 .pitch
2453 .clone()
2454 .ok_or_else(|| Error::InvalidCommand("pitch required for non-rest note".into()))?;
2455 let mut n = Note::new(pitch, cmd.duration.clone());
2456 n.dot_count = cmd.dot_count;
2457 n.tuplet = cmd.tuplet.clone();
2458 n
2459 };
2460
2461 let pos = {
2462 let voice = score
2463 .parts
2464 .get_mut(cmd.part_index)
2465 .ok_or(Error::PartNotFound(cmd.part_index))?
2466 .staves
2467 .get_mut(cmd.staff_index)
2468 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2469 .measures
2470 .get_mut(cmd.measure_index)
2471 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2472 .voices
2473 .get_mut(cmd.voice)
2474 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2475 let pos = cmd.position.min(voice.len());
2476 voice.insert(pos, note);
2477 trim_voice_to_measure(voice, ts_beats);
2478 pos
2479 };
2480 remap_spanners(score, |address| {
2481 if same_voice(
2482 address,
2483 cmd.part_index,
2484 cmd.staff_index,
2485 cmd.measure_index,
2486 cmd.voice,
2487 ) && address.note >= pos
2488 {
2489 let mut shifted = address.clone();
2490 shifted.note += 1;
2491 Some(shifted)
2492 } else {
2493 Some(address.clone())
2494 }
2495 });
2496 Ok(())
2497}
2498
2499fn apply_add_pitch(cmd: &AddPitchCmd, score: &mut Score) -> Result<(), Error> {
2500 let voice = score
2501 .parts
2502 .get_mut(cmd.part_index)
2503 .ok_or(Error::PartNotFound(cmd.part_index))?
2504 .staves
2505 .get_mut(cmd.staff_index)
2506 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2507 .measures
2508 .get_mut(cmd.measure_index)
2509 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2510 .voices
2511 .get_mut(cmd.voice)
2512 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2513 let note = voice
2514 .get_mut(cmd.note_index)
2515 .ok_or(Error::NoteNotFound(cmd.note_index))?;
2516 if note.is_rest {
2517 return Err(Error::InvalidCommand("cannot add pitch to a rest".into()));
2518 }
2519 if !note
2520 .pitches
2521 .iter()
2522 .any(|p| p.step == cmd.pitch.step && p.octave == cmd.pitch.octave)
2523 {
2524 note.pitches.push(cmd.pitch.clone());
2525 }
2526 Ok(())
2527}
2528
2529fn apply_set_duration(cmd: &SetDurationCmd, score: &mut Score) -> Result<(), Error> {
2530 let ts_beats = score.settings.time_signature.total_beats();
2531 {
2532 let voice = score
2533 .parts
2534 .get_mut(cmd.part_index)
2535 .ok_or(Error::PartNotFound(cmd.part_index))?
2536 .staves
2537 .get_mut(cmd.staff_index)
2538 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2539 .measures
2540 .get_mut(cmd.measure_index)
2541 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2542 .voices
2543 .get_mut(cmd.voice)
2544 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2545 let note = voice
2546 .get_mut(cmd.note_index)
2547 .ok_or(Error::NoteNotFound(cmd.note_index))?;
2548 note.duration = cmd.duration.clone();
2549 note.dot_count = cmd.dot_count;
2550 trim_voice_to_measure(voice, ts_beats);
2551 }
2552 prune_orphaned_spanners(score);
2553 Ok(())
2554}
2555
2556fn apply_delete_note(cmd: &DeleteNoteCmd, score: &mut Score) -> Result<(), Error> {
2557 let ts_beats = score.settings.time_signature.total_beats();
2558 let deleted_position = {
2559 let voice = score
2560 .parts
2561 .get_mut(cmd.part_index)
2562 .ok_or(Error::PartNotFound(cmd.part_index))?
2563 .staves
2564 .get_mut(cmd.staff_index)
2565 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2566 .measures
2567 .get_mut(cmd.measure_index)
2568 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2569 .voices
2570 .get_mut(cmd.voice)
2571 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2572 let position = voice.iter().position(|note| note.id == cmd.note_id);
2573 voice.retain(|note| note.id != cmd.note_id);
2574 pad_voice_to_measure(voice, ts_beats);
2575 position
2576 };
2577 if let Some(position) = deleted_position {
2578 remap_spanners(score, |address| {
2579 if !same_voice(
2580 address,
2581 cmd.part_index,
2582 cmd.staff_index,
2583 cmd.measure_index,
2584 cmd.voice,
2585 ) {
2586 return Some(address.clone());
2587 }
2588 if address.note == position {
2589 None
2590 } else if address.note > position {
2591 let mut shifted = address.clone();
2592 shifted.note -= 1;
2593 Some(shifted)
2594 } else {
2595 Some(address.clone())
2596 }
2597 });
2598 } else {
2599 prune_orphaned_spanners(score);
2600 }
2601 Ok(())
2602}
2603
2604fn apply_add_measure(cmd: &AddMeasureCmd, score: &mut Score) -> Result<(), Error> {
2605 let ts = score.settings.time_signature.clone();
2606 let insert_at = cmd.after_index.saturating_add(1);
2607 for part in &mut score.parts {
2608 for staff in &mut part.staves {
2609 let insert_at = insert_at.min(staff.measures.len());
2610 let mut m = Measure::empty(ts.numerator, ts.denominator);
2611 m.number = insert_at as u32 + 1;
2612 staff.measures.insert(insert_at, m);
2613 for (i, measure) in staff.measures.iter_mut().enumerate() {
2614 measure.number = i as u32 + 1;
2615 }
2616 }
2617 }
2618 remap_spanners(score, |address| {
2619 if address.measure >= insert_at {
2620 let mut shifted = address.clone();
2621 shifted.measure += 1;
2622 Some(shifted)
2623 } else {
2624 Some(address.clone())
2625 }
2626 });
2627 Ok(())
2628}
2629
2630fn apply_delete_measure(cmd: &DeleteMeasureCmd, score: &mut Score) -> Result<(), Error> {
2631 for part in &mut score.parts {
2632 for staff in &mut part.staves {
2633 if cmd.measure_index < staff.measures.len() {
2634 staff.measures.remove(cmd.measure_index);
2635 for (i, m) in staff.measures.iter_mut().enumerate() {
2636 m.number = i as u32 + 1;
2637 }
2638 }
2639 }
2640 }
2641 remap_spanners(score, |address| {
2642 if address.measure == cmd.measure_index {
2643 None
2644 } else if address.measure > cmd.measure_index {
2645 let mut shifted = address.clone();
2646 shifted.measure -= 1;
2647 Some(shifted)
2648 } else {
2649 Some(address.clone())
2650 }
2651 });
2652 Ok(())
2653}
2654
2655fn apply_add_hairpin(cmd: &AddHairpinCmd, score: &mut Score) -> Result<(), Error> {
2656 let voice = score
2657 .parts
2658 .get_mut(cmd.part_index)
2659 .ok_or(Error::PartNotFound(cmd.part_index))?
2660 .staves
2661 .get_mut(cmd.staff_index)
2662 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2663 .measures
2664 .get_mut(cmd.measure_index)
2665 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2666 .voices
2667 .get_mut(cmd.voice)
2668 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2669 if cmd.start_note_idx >= voice.len() {
2670 return Err(Error::NoteNotFound(cmd.start_note_idx));
2671 }
2672 if cmd.end_note_idx >= voice.len() {
2673 return Err(Error::NoteNotFound(cmd.end_note_idx));
2674 }
2675 if cmd.start_note_idx >= cmd.end_note_idx {
2676 return Err(Error::InvalidCommand(
2677 "start_note_idx must be less than end_note_idx".into(),
2678 ));
2679 }
2680 for note in voice
2681 .iter_mut()
2682 .take(cmd.end_note_idx + 1)
2683 .skip(cmd.start_note_idx)
2684 {
2685 note.hairpin_start = None;
2686 note.hairpin_end = false;
2687 }
2688 voice[cmd.start_note_idx].hairpin_start = Some(cmd.kind);
2689 voice[cmd.end_note_idx].hairpin_end = true;
2690 Ok(())
2691}
2692
2693fn apply_add_pedal(cmd: &AddPedalCmd, score: &mut Score) -> Result<(), Error> {
2694 let voice = score
2695 .parts
2696 .get_mut(cmd.part_index)
2697 .ok_or(Error::PartNotFound(cmd.part_index))?
2698 .staves
2699 .get_mut(cmd.staff_index)
2700 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2701 .measures
2702 .get_mut(cmd.measure_index)
2703 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2704 .voices
2705 .get_mut(cmd.voice)
2706 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2707 if cmd.start_note_idx >= voice.len() {
2708 return Err(Error::NoteNotFound(cmd.start_note_idx));
2709 }
2710 if cmd.end_note_idx >= voice.len() {
2711 return Err(Error::NoteNotFound(cmd.end_note_idx));
2712 }
2713 if cmd.start_note_idx >= cmd.end_note_idx {
2714 return Err(Error::InvalidCommand(
2715 "start_note_idx must be less than end_note_idx".into(),
2716 ));
2717 }
2718 for note in voice
2719 .iter_mut()
2720 .take(cmd.end_note_idx + 1)
2721 .skip(cmd.start_note_idx)
2722 {
2723 note.pedal_start = false;
2724 note.pedal_end = false;
2725 }
2726 voice[cmd.start_note_idx].pedal_start = true;
2727 voice[cmd.end_note_idx].pedal_end = true;
2728 Ok(())
2729}
2730
2731fn apply_toggle_tie(cmd: &ToggleTieCmd, score: &mut Score) -> Result<(), Error> {
2732 let current_tie_start = {
2733 let v = score
2734 .parts
2735 .get(cmd.part_index)
2736 .ok_or(Error::PartNotFound(cmd.part_index))?
2737 .staves
2738 .get(cmd.staff_index)
2739 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2740 .measures
2741 .get(cmd.measure_index)
2742 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
2743 .voices
2744 .get(cmd.voice)
2745 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
2746 v.get(cmd.note_index)
2747 .ok_or(Error::NoteNotFound(cmd.note_index))?
2748 .tie_start
2749 };
2750 let voice_len = score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index]
2751 .voices[cmd.voice]
2752 .len();
2753 let total_measures = score.parts[cmd.part_index].staves[cmd.staff_index]
2754 .measures
2755 .len();
2756
2757 let new_tie = !current_tie_start;
2758 score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index].voices
2759 [cmd.voice][cmd.note_index]
2760 .tie_start = new_tie;
2761
2762 if cmd.note_index + 1 < voice_len {
2763 score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index].voices
2764 [cmd.voice][cmd.note_index + 1]
2765 .tie_end = new_tie;
2766 } else {
2767 let next_mi = cmd.measure_index + 1;
2768 if next_mi < total_measures {
2769 let next_voice = &mut score.parts[cmd.part_index].staves[cmd.staff_index].measures
2770 [next_mi]
2771 .voices[cmd.voice];
2772 if let Some(n) = next_voice.get_mut(0) {
2773 n.tie_end = new_tie;
2774 }
2775 }
2776 }
2777 Ok(())
2778}
2779
2780fn apply_set_dynamic(cmd: &SetDynamicCmd, score: &mut Score) -> Result<(), Error> {
2781 get_note_mut(
2782 score,
2783 cmd.part_index,
2784 cmd.staff_index,
2785 cmd.measure_index,
2786 cmd.voice,
2787 cmd.note_index,
2788 )?
2789 .dynamic = cmd.dynamic.clone();
2790 Ok(())
2791}
2792
2793fn apply_toggle_articulation(cmd: &ToggleArticulationCmd, score: &mut Score) -> Result<(), Error> {
2794 let note = get_note_mut(
2795 score,
2796 cmd.part_index,
2797 cmd.staff_index,
2798 cmd.measure_index,
2799 cmd.voice,
2800 cmd.note_index,
2801 )?;
2802 if let Some(pos) = note
2803 .articulations
2804 .iter()
2805 .position(|a| a == &cmd.articulation)
2806 {
2807 note.articulations.remove(pos);
2808 } else {
2809 note.articulations.push(cmd.articulation.clone());
2810 }
2811 Ok(())
2812}
2813
2814fn apply_set_time_signature(cmd: &SetTimeSignatureCmd, score: &mut Score) -> Result<(), Error> {
2815 if cmd.numerator == 0 || cmd.denominator == 0 {
2816 return Err(Error::InvalidCommand(
2817 "time signature numerator and denominator must be > 0".into(),
2818 ));
2819 }
2820 if ![1u8, 2, 4, 8, 16, 32].contains(&cmd.denominator) {
2821 return Err(Error::InvalidCommand(format!(
2822 "invalid time signature denominator: {}",
2823 cmd.denominator
2824 )));
2825 }
2826 score.settings.time_signature = TimeSignature {
2827 numerator: cmd.numerator,
2828 denominator: cmd.denominator,
2829 };
2830 let max_beats = score.settings.time_signature.total_beats();
2831 for part in &mut score.parts {
2832 for staff in &mut part.staves {
2833 for measure in &mut staff.measures {
2834 for voice in &mut measure.voices {
2835 trim_voice_to_measure(voice, max_beats);
2836 pad_voice_to_measure(voice, max_beats);
2837 }
2838 }
2839 }
2840 }
2841 Ok(())
2842}
2843
2844fn apply_set_barline(cmd: &SetBarlineCmd, score: &mut Score) -> Result<(), Error> {
2845 for part in &mut score.parts {
2846 for staff in &mut part.staves {
2847 let measure = staff
2848 .measures
2849 .get_mut(cmd.measure_index)
2850 .ok_or(Error::MeasureNotFound(cmd.measure_index))?;
2851 match cmd.side.as_str() {
2852 "left" => measure.barline_left = cmd.barline.clone(),
2853 "right" => measure.barline_right = cmd.barline.clone(),
2854 _ => {
2855 return Err(Error::InvalidCommand(format!(
2856 "invalid barline side: '{}'",
2857 cmd.side
2858 )));
2859 }
2860 }
2861 }
2862 }
2863 Ok(())
2864}
2865
2866fn apply_add_part(cmd: &AddPartCmd, score: &mut Score) -> Result<(), Error> {
2867 if cmd.clefs.is_empty() {
2868 return Err(Error::InvalidCommand(
2869 "AddPart requires at least one clef".into(),
2870 ));
2871 }
2872 let measure_count = score.measure_count();
2873 let ts = score.settings.time_signature.clone();
2874 let mut part = Part::new(&cmd.name, &cmd.short_name);
2875 part.midi_channel = cmd.midi_channel.min(15);
2876 part.midi_program = cmd.midi_program;
2877 for clef_str in &cmd.clefs {
2878 let clef = match clef_str.as_str() {
2879 "Bass" => Clef::Bass,
2880 "Alto" => Clef::Alto,
2881 "Tenor" => Clef::Tenor,
2882 "Percussion" => Clef::Percussion,
2883 _ => Clef::Treble,
2884 };
2885 let mut staff = Staff::new(clef);
2886 for i in 0..measure_count {
2887 let mut m = Measure::empty(ts.numerator, ts.denominator);
2888 m.number = i as u32 + 1;
2889 staff.measures.push(m);
2890 }
2891 part.staves.push(staff);
2892 }
2893 score.parts.push(part);
2894 Ok(())
2895}
2896
2897fn apply_set_clef(cmd: &SetClefCmd, score: &mut Score) -> Result<(), Error> {
2898 score
2899 .parts
2900 .get_mut(cmd.part_index)
2901 .ok_or(Error::PartNotFound(cmd.part_index))?
2902 .staves
2903 .get_mut(cmd.staff_index)
2904 .ok_or(Error::StaffNotFound(cmd.staff_index))?
2905 .clef = cmd.clef.clone();
2906 Ok(())
2907}
2908
2909fn apply_set_part_name(cmd: &SetPartNameCmd, score: &mut Score) -> Result<(), Error> {
2910 let part = score
2911 .parts
2912 .get_mut(cmd.part_index)
2913 .ok_or(Error::PartNotFound(cmd.part_index))?;
2914 part.name = cmd.name.clone();
2915 part.short_name = cmd.short_name.clone();
2916 Ok(())
2917}
2918
2919fn apply_delete_part(cmd: &DeletePartCmd, score: &mut Score) -> Result<(), Error> {
2920 if cmd.part_index >= score.parts.len() {
2921 return Err(Error::PartNotFound(cmd.part_index));
2922 }
2923 score.parts.remove(cmd.part_index);
2924 remap_spanners(score, |address| {
2925 if address.part == cmd.part_index {
2926 None
2927 } else if address.part > cmd.part_index {
2928 let mut shifted = address.clone();
2929 shifted.part -= 1;
2930 Some(shifted)
2931 } else {
2932 Some(address.clone())
2933 }
2934 });
2935 Ok(())
2936}
2937
2938fn apply_reorder_parts(cmd: &ReorderPartsCmd, score: &mut Score) -> Result<(), Error> {
2939 let count = score.parts.len();
2940 if cmd.order.len() != count {
2941 return Err(Error::InvalidCommand(format!(
2942 "part order must contain exactly {count} entries"
2943 )));
2944 }
2945 let mut old_to_new = vec![usize::MAX; count];
2946 for (new_index, &old_index) in cmd.order.iter().enumerate() {
2947 if old_index >= count {
2948 return Err(Error::PartNotFound(old_index));
2949 }
2950 if std::mem::replace(&mut old_to_new[old_index], new_index) != usize::MAX {
2951 return Err(Error::InvalidCommand(format!(
2952 "part order contains duplicate index {old_index}"
2953 )));
2954 }
2955 }
2956 for view in &score.views {
2957 for &part in &view.parts {
2958 if part >= count {
2959 return Err(Error::PartNotFound(part));
2960 }
2961 }
2962 for override_ in &view.staff_kind_overrides {
2963 if override_.staff.part >= count {
2964 return Err(Error::PartNotFound(override_.staff.part));
2965 }
2966 }
2967 }
2968 for spanner in &score.spanners {
2969 for address in [&spanner.start, &spanner.end] {
2970 if address.part >= count {
2971 return Err(Error::PartNotFound(address.part));
2972 }
2973 }
2974 }
2975 for group in &score.part_groups {
2976 if group.first_part > group.last_part || group.last_part >= count {
2977 return Err(Error::InvalidCommand(
2978 "part group references an invalid part range".into(),
2979 ));
2980 }
2981 let mut remapped: Vec<_> = (group.first_part..=group.last_part)
2982 .map(|part| old_to_new[part])
2983 .collect();
2984 remapped.sort_unstable();
2985 if remapped
2986 .windows(2)
2987 .any(|pair| pair[1] != pair[0].saturating_add(1))
2988 {
2989 return Err(Error::InvalidCommand(
2990 "part order would split an existing part group".into(),
2991 ));
2992 }
2993 }
2994
2995 let existing = std::mem::take(&mut score.parts);
2996 score.parts = cmd
2997 .order
2998 .iter()
2999 .map(|&old_index| existing[old_index].clone())
3000 .collect();
3001 for view in &mut score.views {
3002 for part in &mut view.parts {
3003 *part = old_to_new[*part];
3004 }
3005 for override_ in &mut view.staff_kind_overrides {
3006 override_.staff.part = old_to_new[override_.staff.part];
3007 }
3008 }
3009 for group in &mut score.part_groups {
3010 group.first_part = old_to_new[group.first_part];
3011 group.last_part = old_to_new[group.last_part];
3012 if group.first_part > group.last_part {
3013 std::mem::swap(&mut group.first_part, &mut group.last_part);
3014 }
3015 }
3016 remap_spanners(score, |address| {
3017 let mut remapped = address.clone();
3018 remapped.part = old_to_new[address.part];
3019 Some(remapped)
3020 });
3021 Ok(())
3022}
3023
3024fn apply_set_metadata(cmd: &SetMetadataCmd, score: &mut Score) -> Result<(), Error> {
3025 if let Some(v) = &cmd.title {
3026 score.metadata.title = v.clone();
3027 }
3028 if let Some(v) = &cmd.composer {
3029 score.metadata.composer = v.clone();
3030 }
3031 if let Some(v) = &cmd.lyricist {
3032 score.metadata.lyricist = v.clone();
3033 }
3034 if let Some(v) = &cmd.copyright {
3035 score.metadata.copyright = v.clone();
3036 }
3037 if let Some(v) = &cmd.work_number {
3038 score.metadata.work_number = v.clone();
3039 }
3040 if let Some(v) = &cmd.movement_title {
3041 score.metadata.movement_title = v.clone();
3042 }
3043 Ok(())
3044}
3045
3046fn apply_set_midi_instrument(cmd: &SetMidiInstrumentCmd, score: &mut Score) -> Result<(), Error> {
3047 let part = score
3048 .parts
3049 .get_mut(cmd.part_index)
3050 .ok_or(Error::PartNotFound(cmd.part_index))?;
3051 part.midi_channel = cmd.midi_channel.min(15);
3052 part.midi_program = cmd.midi_program;
3053 Ok(())
3054}
3055
3056fn apply_set_percussion_kit(cmd: &SetPercussionKitCmd, score: &mut Score) -> Result<(), Error> {
3057 validate_percussion_kit(&cmd.instruments)?;
3058 let part = score
3059 .parts
3060 .get_mut(cmd.part_index)
3061 .ok_or(Error::PartNotFound(cmd.part_index))?;
3062 part.percussion_instruments = cmd.instruments.clone();
3063 Ok(())
3064}
3065
3066fn apply_set_instrument_definition(
3067 cmd: &SetInstrumentDefinitionCmd,
3068 score: &mut Score,
3069) -> Result<(), Error> {
3070 if let Some(definition) = &cmd.definition {
3071 validate_instrument_definition(definition)?;
3072 }
3073 let part = score
3074 .parts
3075 .get_mut(cmd.part_index)
3076 .ok_or(Error::PartNotFound(cmd.part_index))?;
3077 part.instrument = cmd.definition.clone();
3078 Ok(())
3079}
3080
3081fn apply_set_measure_instrument_change(
3082 cmd: &SetMeasureInstrumentChangeCmd,
3083 score: &mut Score,
3084) -> Result<(), Error> {
3085 if let Some(definition) = &cmd.definition {
3086 validate_instrument_definition(definition)?;
3087 }
3088 score
3089 .parts
3090 .get_mut(cmd.part_index)
3091 .ok_or(Error::PartNotFound(cmd.part_index))?
3092 .staves
3093 .get_mut(cmd.staff_index)
3094 .ok_or(Error::StaffNotFound(cmd.staff_index))?
3095 .measures
3096 .get_mut(cmd.measure_index)
3097 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
3098 .instrument_change = cmd.definition.clone();
3099 Ok(())
3100}
3101
3102fn apply_set_measure_tablature_change(
3103 cmd: &SetMeasureTablatureChangeCmd,
3104 score: &mut Score,
3105) -> Result<(), Error> {
3106 let staff = score
3107 .parts
3108 .get_mut(cmd.part_index)
3109 .ok_or(Error::PartNotFound(cmd.part_index))?
3110 .staves
3111 .get_mut(cmd.staff_index)
3112 .ok_or(Error::StaffNotFound(cmd.staff_index))?;
3113 let base = staff.tablature.as_ref().ok_or_else(|| {
3114 Error::InvalidCommand("tablature change requires a staff tablature configuration".into())
3115 })?;
3116 if let Some(config) = &cmd.config {
3117 validate_tablature_config(config)?;
3118 if config.lines != base.lines {
3119 return Err(Error::InvalidCommand(format!(
3120 "tablature change line count {} must match staff line count {}",
3121 config.lines, base.lines
3122 )));
3123 }
3124 }
3125 staff
3126 .measures
3127 .get_mut(cmd.measure_index)
3128 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
3129 .tablature_change = cmd.config.clone();
3130 Ok(())
3131}
3132
3133fn validate_tablature_config(config: &TablatureConfig) -> Result<(), Error> {
3134 if !(1..=64).contains(&config.lines) {
3135 return Err(Error::InvalidCommand(format!(
3136 "tablature line count {} is outside 1..=64",
3137 config.lines
3138 )));
3139 }
3140 if config.tuning_midi.len() > usize::from(config.lines) {
3141 return Err(Error::InvalidCommand(format!(
3142 "tablature has {} tunings for {} lines",
3143 config.tuning_midi.len(),
3144 config.lines
3145 )));
3146 }
3147 if let Some(midi) = config
3148 .tuning_midi
3149 .iter()
3150 .copied()
3151 .find(|midi| !(0..=127).contains(midi))
3152 {
3153 return Err(Error::InvalidCommand(format!(
3154 "tablature tuning MIDI {midi} is outside 0..=127"
3155 )));
3156 }
3157 Ok(())
3158}
3159
3160fn validate_percussion_kit(instruments: &[PercussionInstrument]) -> Result<(), Error> {
3161 let mut ids = std::collections::HashSet::new();
3162 for instrument in instruments {
3163 if instrument.id.trim().is_empty() {
3164 return Err(Error::InvalidCommand(
3165 "percussion instrument id must not be empty".into(),
3166 ));
3167 }
3168 if !ids.insert(instrument.id.as_str()) {
3169 return Err(Error::InvalidCommand(format!(
3170 "percussion instrument id '{}' is duplicated",
3171 instrument.id
3172 )));
3173 }
3174 if let Some(position) = instrument.staff_position
3175 && !(-32..=32).contains(&position)
3176 {
3177 return Err(Error::InvalidCommand(format!(
3178 "percussion staff position {position} is outside -32..=32"
3179 )));
3180 }
3181 if let Some(voice) = instrument.preferred_voice
3182 && !(1..=4).contains(&voice)
3183 {
3184 return Err(Error::InvalidCommand(format!(
3185 "percussion preferred voice {voice} is outside 1..=4"
3186 )));
3187 }
3188 if instrument
3189 .techniques
3190 .iter()
3191 .any(|technique| technique.trim().is_empty() || technique.len() > 128)
3192 {
3193 return Err(Error::InvalidCommand(
3194 "percussion techniques must be non-empty and at most 128 bytes".into(),
3195 ));
3196 }
3197 }
3198 Ok(())
3199}
3200
3201fn validate_instrument_definition(definition: &InstrumentDefinition) -> Result<(), Error> {
3202 if definition.id.trim().is_empty() {
3203 return Err(Error::InvalidCommand(
3204 "instrument definition id must not be empty".into(),
3205 ));
3206 }
3207 if !(1..=64).contains(&definition.staff_count) {
3208 return Err(Error::InvalidCommand(format!(
3209 "instrument staff count {} is outside 1..=64",
3210 definition.staff_count
3211 )));
3212 }
3213 if definition.midi_channel > 15 {
3214 return Err(Error::InvalidCommand(format!(
3215 "instrument MIDI channel {} is outside 0..=15",
3216 definition.midi_channel
3217 )));
3218 }
3219 for (name, range) in [
3220 ("written", definition.written_range),
3221 ("sounding", definition.sounding_range),
3222 ] {
3223 if let Some(InstrumentRange { lowest, highest }) = range
3224 && lowest > highest
3225 {
3226 return Err(Error::InvalidCommand(format!(
3227 "instrument {name} range has lowest MIDI note {lowest} above highest {highest}"
3228 )));
3229 }
3230 }
3231 Ok(())
3232}
3233
3234fn apply_upsert_score_view(cmd: &UpsertScoreViewCmd, score: &mut Score) -> Result<(), Error> {
3235 validate_score_view(&cmd.view, score)?;
3236 if let Some(index) = score.views.iter().position(|view| view.id == cmd.view.id) {
3237 score.views[index] = cmd.view.clone();
3238 } else {
3239 score.views.push(cmd.view.clone());
3240 }
3241 Ok(())
3242}
3243
3244fn apply_remove_score_view(cmd: &RemoveScoreViewCmd, score: &mut Score) -> Result<(), Error> {
3245 let index = score
3246 .views
3247 .iter()
3248 .position(|view| view.id == cmd.id)
3249 .ok_or_else(|| Error::InvalidCommand(format!("score view '{}' does not exist", cmd.id)))?;
3250 score.views.remove(index);
3251 Ok(())
3252}
3253
3254fn validate_score_view(view: &ScoreView, score: &Score) -> Result<(), Error> {
3255 if view.id.trim().is_empty() || view.name.trim().is_empty() {
3256 return Err(Error::InvalidCommand(
3257 "score view id and name must not be empty".into(),
3258 ));
3259 }
3260 if view.parts.is_empty() {
3261 return Err(Error::InvalidCommand(
3262 "score view must select at least one part".into(),
3263 ));
3264 }
3265 let mut selected = vec![false; score.parts.len()];
3266 for &part in &view.parts {
3267 if part >= score.parts.len() {
3268 return Err(Error::PartNotFound(part));
3269 }
3270 if std::mem::replace(&mut selected[part], true) {
3271 return Err(Error::InvalidCommand(format!(
3272 "score view '{}' selects part {part} more than once",
3273 view.id
3274 )));
3275 }
3276 }
3277 if view.layout.measures_per_row.is_some_and(|value| value == 0) {
3278 return Err(Error::InvalidCommand(
3279 "score view measures per row must be greater than zero".into(),
3280 ));
3281 }
3282 if view
3283 .layout
3284 .typed_style_overrides
3285 .iter()
3286 .any(|override_| !override_.value.is_finite() || !(0.05..=64.0).contains(&override_.value))
3287 {
3288 return Err(Error::InvalidCommand(
3289 "score view typed style override values must be finite and within 0.05..=64".into(),
3290 ));
3291 }
3292 for reference in &view.layout.hidden_staves {
3293 let Some(part) = score.parts.get(reference.part) else {
3294 return Err(Error::PartNotFound(reference.part));
3295 };
3296 if reference.staff >= part.staves.len() {
3297 return Err(Error::StaffNotFound(reference.staff));
3298 }
3299 if !selected[reference.part] {
3300 return Err(Error::InvalidCommand(
3301 "score view cannot hide a staff outside its selected parts".into(),
3302 ));
3303 }
3304 }
3305 let mut overridden = std::collections::HashSet::new();
3306 for override_ in &view.staff_kind_overrides {
3307 let reference = override_.staff;
3308 let Some(part) = score.parts.get(reference.part) else {
3309 return Err(Error::PartNotFound(reference.part));
3310 };
3311 if reference.staff >= part.staves.len() {
3312 return Err(Error::StaffNotFound(reference.staff));
3313 }
3314 if !selected[reference.part] {
3315 return Err(Error::InvalidCommand(
3316 "score view cannot override a staff outside its selected parts".into(),
3317 ));
3318 }
3319 if !overridden.insert((reference.part, reference.staff)) {
3320 return Err(Error::InvalidCommand(
3321 "score view may override each staff kind at most once".into(),
3322 ));
3323 }
3324 if override_.kind == StaffKind::Tablature
3325 && part.staves[reference.staff].tablature.is_none()
3326 {
3327 return Err(Error::InvalidCommand(
3328 "tablature score view requires a tablature configuration".into(),
3329 ));
3330 }
3331 }
3332 let measure_count = score.measure_count();
3333 for &break_index in view
3334 .layout
3335 .system_breaks
3336 .iter()
3337 .chain(view.layout.page_breaks.iter())
3338 {
3339 if break_index >= measure_count {
3340 return Err(Error::InvalidCommand(format!(
3341 "score view break measure {break_index} is out of range"
3342 )));
3343 }
3344 }
3345 for (key, value) in &view.layout.style_overrides {
3346 if key.trim().is_empty() || value.len() > 4096 {
3347 return Err(Error::InvalidCommand(
3348 "score view style overrides need a non-empty key and a value of at most 4096 bytes"
3349 .into(),
3350 ));
3351 }
3352 }
3353 Ok(())
3354}
3355
3356fn apply_set_transpose(cmd: &SetTransposeCmd, score: &mut Score) -> Result<(), Error> {
3357 score
3358 .parts
3359 .get_mut(cmd.part_index)
3360 .ok_or(Error::PartNotFound(cmd.part_index))?
3361 .staves
3362 .get_mut(cmd.staff_index)
3363 .ok_or(Error::StaffNotFound(cmd.staff_index))?
3364 .transpose_semitones = cmd.semitones;
3365 Ok(())
3366}
3367
3368fn apply_transpose_staff_region(
3369 cmd: &TransposeStaffRegionCmd,
3370 score: &mut Score,
3371) -> Result<(), Error> {
3372 *score = transpose_staff_region_checked(
3373 score,
3374 cmd.part_index,
3375 cmd.staff_index,
3376 cmd.start_measure,
3377 cmd.end_measure,
3378 cmd.semitones,
3379 cmd.target,
3380 )?;
3381 Ok(())
3382}
3383
3384fn apply_exchange_voices(cmd: &ExchangeVoicesCmd, score: &mut Score) -> Result<(), Error> {
3385 if cmd.first_voice >= 4 {
3386 return Err(Error::VoiceOutOfRange(cmd.first_voice));
3387 }
3388 if cmd.second_voice >= 4 {
3389 return Err(Error::VoiceOutOfRange(cmd.second_voice));
3390 }
3391 if cmd.first_voice == cmd.second_voice {
3392 return Err(Error::InvalidCommand(
3393 "exchange voices requires two distinct voices".into(),
3394 ));
3395 }
3396 let start = cmd.start_measure.min(cmd.end_measure);
3397 let end = cmd.start_measure.max(cmd.end_measure);
3398 let staff = score
3399 .parts
3400 .get_mut(cmd.part_index)
3401 .ok_or(Error::PartNotFound(cmd.part_index))?
3402 .staves
3403 .get_mut(cmd.staff_index)
3404 .ok_or(Error::StaffNotFound(cmd.staff_index))?;
3405 if end >= staff.measures.len() {
3406 return Err(Error::MeasureNotFound(end));
3407 }
3408 for measure in &mut staff.measures[start..=end] {
3409 measure.voices.swap(cmd.first_voice, cmd.second_voice);
3410 measure
3411 .source_voice_numbers
3412 .swap(cmd.first_voice, cmd.second_voice);
3413 }
3414 remap_spanners(score, |address| {
3415 if address.part != cmd.part_index
3416 || address.staff != cmd.staff_index
3417 || address.measure < start
3418 || address.measure > end
3419 {
3420 return Some(address.clone());
3421 }
3422 let mut remapped = address.clone();
3423 if address.voice == cmd.first_voice {
3424 remapped.voice = cmd.second_voice;
3425 } else if address.voice == cmd.second_voice {
3426 remapped.voice = cmd.first_voice;
3427 }
3428 Some(remapped)
3429 });
3430 Ok(())
3431}
3432
3433fn apply_move_or_copy_voice_range(
3434 cmd: &MoveOrCopyVoiceRangeCmd,
3435 score: &mut Score,
3436) -> Result<(), Error> {
3437 if cmd.source_start.part != cmd.source_end.part
3438 || cmd.source_start.staff != cmd.source_end.staff
3439 || cmd.source_start.voice != cmd.source_end.voice
3440 {
3441 return Err(Error::InvalidCommand(
3442 "move or copy source endpoints must share part, staff, and voice".into(),
3443 ));
3444 }
3445 if cmd.source_start.voice >= 4 {
3446 return Err(Error::VoiceOutOfRange(cmd.source_start.voice));
3447 }
3448 let source_from = cmd.source_start.measure.min(cmd.source_end.measure);
3449 let source_to = cmd.source_start.measure.max(cmd.source_end.measure);
3450 let source_count = source_to - source_from + 1;
3451 let target_end = cmd
3452 .target
3453 .measure
3454 .checked_add(source_count - 1)
3455 .ok_or_else(|| Error::InvalidCommand("voice range target overflows".into()))?;
3456 if cmd.move_source
3457 && cmd.target.part == cmd.source_start.part
3458 && cmd.target.staff == cmd.source_start.staff
3459 && cmd.target.voice == cmd.source_start.voice
3460 && cmd.target.measure <= source_to
3461 && target_end >= source_from
3462 {
3463 return Err(Error::InvalidCommand(
3464 "moving a voice range onto itself is not supported".into(),
3465 ));
3466 }
3467
3468 let fragment = extract_score_fragment(
3469 score,
3470 &[ScoreFragmentSelection {
3471 start: cmd.source_start.clone(),
3472 end: cmd.source_end.clone(),
3473 }],
3474 )?;
3475 apply_paste_score_fragment(
3476 &PasteScoreFragmentCmd {
3477 fragment,
3478 target: cmd.target.clone(),
3479 policy: ScoreFragmentPastePolicy::Replace,
3480 },
3481 score,
3482 )?;
3483 if !cmd.move_source {
3484 return Ok(());
3485 }
3486
3487 let settings_time_signature = score.settings.time_signature.clone();
3488 let staff = score
3489 .parts
3490 .get_mut(cmd.source_start.part)
3491 .ok_or(Error::PartNotFound(cmd.source_start.part))?
3492 .staves
3493 .get_mut(cmd.source_start.staff)
3494 .ok_or(Error::StaffNotFound(cmd.source_start.staff))?;
3495 if source_to >= staff.measures.len() {
3496 return Err(Error::MeasureNotFound(source_to));
3497 }
3498 for measure in &mut staff.measures[source_from..=source_to] {
3499 let signature = measure
3500 .time_sig
3501 .as_ref()
3502 .unwrap_or(&settings_time_signature);
3503 let mut replacement = Vec::new();
3504 pad_voice_to_measure(&mut replacement, signature.total_beats());
3505 measure.voices[cmd.source_start.voice] = replacement;
3506 measure.source_voice_numbers[cmd.source_start.voice] = None;
3507 }
3508 prune_orphaned_spanners(score);
3509 Ok(())
3510}
3511
3512fn effective_staff_measure_beats(
3513 staff: &Staff,
3514 default_time_signature: &TimeSignature,
3515 measure_index: usize,
3516) -> Result<f64, Error> {
3517 let mut time_signature = default_time_signature.clone();
3518 for measure in staff.measures.iter().take(measure_index.saturating_add(1)) {
3519 if let Some(signature) = &measure.time_sig {
3520 time_signature = signature.clone();
3521 }
3522 }
3523 if staff.measures.get(measure_index).is_none() {
3524 return Err(Error::MeasureNotFound(measure_index));
3525 }
3526 Ok(time_signature.total_beats())
3527}
3528
3529fn normalized_structural_measure_range(start: usize, end: usize) -> (usize, usize) {
3530 (start.min(end), start.max(end))
3531}
3532
3533fn apply_implode_staves(cmd: &ImplodeStavesCmd, score: &mut Score) -> Result<(), Error> {
3534 if !(2..=4).contains(&cmd.source_staves.len()) {
3535 return Err(Error::InvalidCommand(
3536 "implode requires two to four source staves".into(),
3537 ));
3538 }
3539 let mut unique_staves = BTreeSet::new();
3540 for &staff_index in &cmd.source_staves {
3541 if !unique_staves.insert(staff_index) {
3542 return Err(Error::InvalidCommand(
3543 "implode source staves must be distinct".into(),
3544 ));
3545 }
3546 }
3547 if !unique_staves.contains(&cmd.target_staff) {
3548 return Err(Error::InvalidCommand(
3549 "implode target staff must be included in source staves".into(),
3550 ));
3551 }
3552 let (start, end) = normalized_structural_measure_range(cmd.start_measure, cmd.end_measure);
3553 let part = score
3554 .parts
3555 .get(cmd.part_index)
3556 .ok_or(Error::PartNotFound(cmd.part_index))?;
3557 for &staff_index in &cmd.source_staves {
3558 if part.staves.get(staff_index).is_none() {
3559 return Err(Error::StaffNotFound(staff_index));
3560 }
3561 }
3562
3563 for measure_index in start..=end {
3567 let mut expected_beats: Option<f64> = None;
3568 for &staff_index in &cmd.source_staves {
3569 let staff = &part.staves[staff_index];
3570 let measure = staff
3571 .measures
3572 .get(measure_index)
3573 .ok_or(Error::MeasureNotFound(measure_index))?;
3574 let beats = effective_staff_measure_beats(
3575 staff,
3576 &score.settings.time_signature,
3577 measure_index,
3578 )?;
3579 if let Some(expected) = expected_beats
3580 && (expected - beats).abs() > 1e-9
3581 {
3582 return Err(Error::InvalidCommand(
3583 "implode source staves must have matching measure durations".into(),
3584 ));
3585 }
3586 expected_beats = Some(beats);
3587 if measure.voices[1..].iter().any(|voice| !voice.is_empty()) {
3588 return Err(Error::InvalidCommand(
3589 "implode requires empty secondary source voices".into(),
3590 ));
3591 }
3592 if measure.voices[0]
3593 .iter()
3594 .any(|note| note.cross_staff.is_some())
3595 {
3596 return Err(Error::InvalidCommand(
3597 "implode does not support cross-staff source notes".into(),
3598 ));
3599 }
3600 let total: f64 = measure.voices[0].iter().map(Note::beats).sum();
3601 if total > beats + 1e-9 {
3602 return Err(Error::InvalidCommand(
3603 "implode source voice exceeds its measure duration".into(),
3604 ));
3605 }
3606 }
3607 }
3608
3609 for measure_index in start..=end {
3610 let transferred: Vec<(Vec<Note>, Option<u32>)> = cmd
3611 .source_staves
3612 .iter()
3613 .map(|&staff_index| {
3614 let measure =
3615 &score.parts[cmd.part_index].staves[staff_index].measures[measure_index];
3616 (measure.voices[0].clone(), measure.source_voice_numbers[0])
3617 })
3618 .collect();
3619 {
3620 let target =
3621 &mut score.parts[cmd.part_index].staves[cmd.target_staff].measures[measure_index];
3622 target.voices = [Vec::new(), Vec::new(), Vec::new(), Vec::new()];
3623 target.source_voice_numbers = [None; 4];
3624 for (voice_index, (notes, source_voice_number)) in transferred.into_iter().enumerate() {
3625 target.voices[voice_index] = notes;
3626 target.source_voice_numbers[voice_index] = source_voice_number;
3627 }
3628 }
3629 for &staff_index in &cmd.source_staves {
3630 if staff_index == cmd.target_staff {
3631 continue;
3632 }
3633 let beats = effective_staff_measure_beats(
3634 &score.parts[cmd.part_index].staves[staff_index],
3635 &score.settings.time_signature,
3636 measure_index,
3637 )?;
3638 let mut rest_voice = Vec::new();
3639 pad_voice_to_measure(&mut rest_voice, beats);
3640 let measure =
3641 &mut score.parts[cmd.part_index].staves[staff_index].measures[measure_index];
3642 measure.voices = [rest_voice, Vec::new(), Vec::new(), Vec::new()];
3643 measure.source_voice_numbers = [None; 4];
3644 }
3645 }
3646 remap_spanners(score, |address| {
3647 if address.part != cmd.part_index || address.measure < start || address.measure > end {
3648 return Some(address.clone());
3649 }
3650 let voice = cmd
3651 .source_staves
3652 .iter()
3653 .position(|&staff| staff == address.staff);
3654 if address.voice != 0 {
3655 return Some(address.clone());
3656 }
3657 voice
3658 .map(|voice| NoteAddr {
3659 staff: cmd.target_staff,
3660 voice,
3661 ..address.clone()
3662 })
3663 .or_else(|| Some(address.clone()))
3664 });
3665 Ok(())
3666}
3667
3668fn apply_explode_voices(cmd: &ExplodeVoicesCmd, score: &mut Score) -> Result<(), Error> {
3669 if !(2..=4).contains(&cmd.target_staves.len()) {
3670 return Err(Error::InvalidCommand(
3671 "explode requires two to four target staves".into(),
3672 ));
3673 }
3674 if cmd.target_staves.first() != Some(&cmd.source_staff) {
3675 return Err(Error::InvalidCommand(
3676 "explode target staves must begin with the source staff".into(),
3677 ));
3678 }
3679 let mut unique_staves = BTreeSet::new();
3680 for &staff_index in &cmd.target_staves {
3681 if !unique_staves.insert(staff_index) {
3682 return Err(Error::InvalidCommand(
3683 "explode target staves must be distinct".into(),
3684 ));
3685 }
3686 }
3687 let (start, end) = normalized_structural_measure_range(cmd.start_measure, cmd.end_measure);
3688 let part = score
3689 .parts
3690 .get(cmd.part_index)
3691 .ok_or(Error::PartNotFound(cmd.part_index))?;
3692 let source = part
3693 .staves
3694 .get(cmd.source_staff)
3695 .ok_or(Error::StaffNotFound(cmd.source_staff))?;
3696 for &staff_index in &cmd.target_staves {
3697 if part.staves.get(staff_index).is_none() {
3698 return Err(Error::StaffNotFound(staff_index));
3699 }
3700 }
3701
3702 for measure_index in start..=end {
3703 let expected =
3704 effective_staff_measure_beats(source, &score.settings.time_signature, measure_index)?;
3705 let source_measure = source
3706 .measures
3707 .get(measure_index)
3708 .ok_or(Error::MeasureNotFound(measure_index))?;
3709 for voice_index in cmd.target_staves.len()..4 {
3710 if !source_measure.voices[voice_index].is_empty() {
3711 return Err(Error::InvalidCommand(
3712 "explode would discard a source voice without a target staff".into(),
3713 ));
3714 }
3715 }
3716 for voice_index in 0..cmd.target_staves.len() {
3717 if source_measure.voices[voice_index]
3718 .iter()
3719 .any(|note| note.cross_staff.is_some())
3720 {
3721 return Err(Error::InvalidCommand(
3722 "explode does not support cross-staff source notes".into(),
3723 ));
3724 }
3725 let total: f64 = source_measure.voices[voice_index]
3726 .iter()
3727 .map(Note::beats)
3728 .sum();
3729 if total > expected + 1e-9 {
3730 return Err(Error::InvalidCommand(
3731 "explode source voice exceeds its measure duration".into(),
3732 ));
3733 }
3734 }
3735 for &staff_index in cmd.target_staves.iter().skip(1) {
3736 let target = &part.staves[staff_index];
3737 let target_beats = effective_staff_measure_beats(
3738 target,
3739 &score.settings.time_signature,
3740 measure_index,
3741 )?;
3742 if (target_beats - expected).abs() > 1e-9 {
3743 return Err(Error::InvalidCommand(
3744 "explode target staves must have matching measure durations".into(),
3745 ));
3746 }
3747 let measure = target
3748 .measures
3749 .get(measure_index)
3750 .ok_or(Error::MeasureNotFound(measure_index))?;
3751 if measure.voices[0].iter().any(|note| !note.is_rest)
3752 || measure.voices[1..].iter().any(|voice| !voice.is_empty())
3753 || measure.source_voice_numbers.iter().any(Option::is_some)
3754 {
3755 return Err(Error::InvalidCommand(
3756 "explode destination staff must contain only an unnumbered rest voice".into(),
3757 ));
3758 }
3759 }
3760 }
3761
3762 for measure_index in start..=end {
3763 let transferred: Vec<(Vec<Note>, Option<u32>)> = (0..cmd.target_staves.len())
3764 .map(|voice_index| {
3765 let measure =
3766 &score.parts[cmd.part_index].staves[cmd.source_staff].measures[measure_index];
3767 (
3768 measure.voices[voice_index].clone(),
3769 measure.source_voice_numbers[voice_index],
3770 )
3771 })
3772 .collect();
3773 for (voice_index, &staff_index) in cmd.target_staves.iter().enumerate() {
3774 let (notes, source_voice_number) = &transferred[voice_index];
3775 let target =
3776 &mut score.parts[cmd.part_index].staves[staff_index].measures[measure_index];
3777 target.voices = [notes.clone(), Vec::new(), Vec::new(), Vec::new()];
3778 target.source_voice_numbers = [*source_voice_number, None, None, None];
3779 }
3780 }
3781 remap_spanners(score, |address| {
3782 if address.part != cmd.part_index
3783 || address.staff != cmd.source_staff
3784 || address.measure < start
3785 || address.measure > end
3786 || address.voice >= cmd.target_staves.len()
3787 {
3788 return Some(address.clone());
3789 }
3790 Some(NoteAddr {
3791 staff: cmd.target_staves[address.voice],
3792 voice: 0,
3793 ..address.clone()
3794 })
3795 });
3796 Ok(())
3797}
3798
3799fn clear_derived_chord_note_notation(note: &mut Note) {
3800 note.articulations.clear();
3801 note.dynamic = None;
3802 note.stem_up = None;
3803 note.hairpin_start = None;
3804 note.hairpin_end = false;
3805 note.chord_symbol = None;
3806 note.ottava_start = None;
3807 note.ottava_end = false;
3808 note.lyric = None;
3809 note.pedal_start = false;
3810 note.pedal_end = false;
3811 note.slur_start = false;
3812 note.slur_end = false;
3813 note.arpeggiate = None;
3814 note.technique_text = None;
3815 note.glissando_start = false;
3816 note.glissando_end = false;
3817 note.cross_staff = None;
3818 note.fingering = None;
3819 note.fingerings.clear();
3820 note.string_number = None;
3821 note.trill_line_start = false;
3822 note.trill_line_end = false;
3823 note.guitar_technique = None;
3824 note.guitar_bend_alter_cents = None;
3825 note.guitar_bend_curve.clear();
3826}
3827
3828fn exploded_chord_note(source: &Note, pitch_index: usize) -> Note {
3829 if pitch_index == 0 {
3830 let mut retained = source.clone();
3831 if !retained.is_rest {
3832 retained.pitches = vec![source.pitches[0].clone()];
3833 retained.tab_positions = source.tab_positions.first().cloned().into_iter().collect();
3834 retained.tab_position = retained.tab_positions.first().cloned();
3835 }
3836 return retained;
3837 }
3838 let mut derived = source.clone();
3839 derived.id = Uuid::new_v4().to_string();
3840 clear_derived_chord_note_notation(&mut derived);
3841 if derived.is_rest || pitch_index >= source.pitches.len() {
3842 derived.is_rest = true;
3843 derived.is_unpitched = false;
3844 derived.pitches.clear();
3845 derived.tab_position = None;
3846 derived.tab_positions.clear();
3847 } else {
3848 derived.pitches = vec![source.pitches[pitch_index].clone()];
3849 derived.tab_positions = source
3850 .tab_positions
3851 .get(pitch_index)
3852 .cloned()
3853 .into_iter()
3854 .collect();
3855 derived.tab_position = derived.tab_positions.first().cloned();
3856 }
3857 derived
3858}
3859
3860fn apply_explode_chord_pitches(
3861 cmd: &ExplodeChordPitchesCmd,
3862 score: &mut Score,
3863) -> Result<(), Error> {
3864 if !(2..=4).contains(&cmd.target_staves.len()) {
3865 return Err(Error::InvalidCommand(
3866 "chord explode requires two to four target staves".into(),
3867 ));
3868 }
3869 if cmd.target_staves.first() != Some(&cmd.source_staff) {
3870 return Err(Error::InvalidCommand(
3871 "chord explode target staves must begin with the source staff".into(),
3872 ));
3873 }
3874 let mut unique_staves = BTreeSet::new();
3875 for &staff_index in &cmd.target_staves {
3876 if !unique_staves.insert(staff_index) {
3877 return Err(Error::InvalidCommand(
3878 "chord explode target staves must be distinct".into(),
3879 ));
3880 }
3881 }
3882 let (start, end) = normalized_structural_measure_range(cmd.start_measure, cmd.end_measure);
3883 let part = score
3884 .parts
3885 .get(cmd.part_index)
3886 .ok_or(Error::PartNotFound(cmd.part_index))?;
3887 let source = part
3888 .staves
3889 .get(cmd.source_staff)
3890 .ok_or(Error::StaffNotFound(cmd.source_staff))?;
3891 for &staff_index in &cmd.target_staves {
3892 if part.staves.get(staff_index).is_none() {
3893 return Err(Error::StaffNotFound(staff_index));
3894 }
3895 }
3896 for measure_index in start..=end {
3897 let expected =
3898 effective_staff_measure_beats(source, &score.settings.time_signature, measure_index)?;
3899 let source_measure = source
3900 .measures
3901 .get(measure_index)
3902 .ok_or(Error::MeasureNotFound(measure_index))?;
3903 for note in &source_measure.voices[0] {
3904 if note.tuplet.is_some() {
3905 return Err(Error::InvalidCommand(
3906 "chord explode does not support tuplets".into(),
3907 ));
3908 }
3909 if note.cross_staff.is_some() {
3910 return Err(Error::InvalidCommand(
3911 "chord explode does not support cross-staff source notes".into(),
3912 ));
3913 }
3914 if note.is_unpitched || note.is_grace || note.is_cue {
3915 return Err(Error::InvalidCommand(
3916 "chord explode does not support unpitched, grace, or cue notes".into(),
3917 ));
3918 }
3919 if !note.is_rest
3920 && (note.pitches.is_empty() || note.pitches.len() > cmd.target_staves.len())
3921 {
3922 return Err(Error::InvalidCommand(
3923 "chord explode pitch count must fit target staves".into(),
3924 ));
3925 }
3926 }
3927 let total: f64 = source_measure.voices[0].iter().map(Note::beats).sum();
3928 if total > expected + 1e-9 {
3929 return Err(Error::InvalidCommand(
3930 "chord explode source voice exceeds its measure duration".into(),
3931 ));
3932 }
3933 for &staff_index in cmd.target_staves.iter().skip(1) {
3934 let target = &part.staves[staff_index];
3935 let target_beats = effective_staff_measure_beats(
3936 target,
3937 &score.settings.time_signature,
3938 measure_index,
3939 )?;
3940 if (target_beats - expected).abs() > 1e-9 {
3941 return Err(Error::InvalidCommand(
3942 "chord explode target staves must have matching measure durations".into(),
3943 ));
3944 }
3945 let measure = target
3946 .measures
3947 .get(measure_index)
3948 .ok_or(Error::MeasureNotFound(measure_index))?;
3949 if measure.voices[0].iter().any(|note| !note.is_rest)
3950 || measure.voices[1..].iter().any(|voice| !voice.is_empty())
3951 || measure.source_voice_numbers.iter().any(Option::is_some)
3952 {
3953 return Err(Error::InvalidCommand(
3954 "chord explode destination staff must contain only an unnumbered rest voice"
3955 .into(),
3956 ));
3957 }
3958 }
3959 }
3960 for measure_index in start..=end {
3961 let source_measure =
3962 &score.parts[cmd.part_index].staves[cmd.source_staff].measures[measure_index];
3963 let source_voice_number = source_measure.source_voice_numbers[0];
3964 let mut exploded = vec![Vec::new(); cmd.target_staves.len()];
3965 for note in &source_measure.voices[0] {
3966 for (pitch_index, voice) in exploded.iter_mut().enumerate() {
3967 voice.push(exploded_chord_note(note, pitch_index));
3968 }
3969 }
3970 for (index, &staff_index) in cmd.target_staves.iter().enumerate() {
3971 let target =
3972 &mut score.parts[cmd.part_index].staves[staff_index].measures[measure_index];
3973 target.voices = [exploded[index].clone(), Vec::new(), Vec::new(), Vec::new()];
3974 target.source_voice_numbers = [source_voice_number, None, None, None];
3975 }
3976 }
3977 Ok(())
3978}
3979
3980fn scaled_duration(duration: &Duration, scale: DurationScale) -> Option<Duration> {
3981 match (duration, scale) {
3982 (Duration::Whole, DurationScale::Double) | (Duration::SixtyFourth, DurationScale::Half) => {
3983 None
3984 }
3985 (Duration::Whole, DurationScale::Half) | (Duration::Half, DurationScale::Double) => {
3986 Some(Duration::Half)
3987 }
3988 (Duration::Half, DurationScale::Half) | (Duration::Quarter, DurationScale::Double) => {
3989 Some(Duration::Quarter)
3990 }
3991 (Duration::Quarter, DurationScale::Half) | (Duration::Eighth, DurationScale::Double) => {
3992 Some(Duration::Eighth)
3993 }
3994 (Duration::Eighth, DurationScale::Half) | (Duration::Sixteenth, DurationScale::Double) => {
3995 Some(Duration::Sixteenth)
3996 }
3997 (Duration::Sixteenth, DurationScale::Half)
3998 | (Duration::ThirtySecond, DurationScale::Double) => Some(Duration::ThirtySecond),
3999 (Duration::ThirtySecond, DurationScale::Half)
4000 | (Duration::SixtyFourth, DurationScale::Double) => Some(Duration::SixtyFourth),
4001 }
4002}
4003
4004fn uniform_tuplet_ratio(voice: &[Note]) -> Result<Option<TupletInfo>, Error> {
4005 let mut ratio: Option<TupletInfo> = None;
4006 for note in voice {
4007 let Some(tuplet) = ¬e.tuplet else {
4008 continue;
4009 };
4010 if tuplet.actual_notes == 0 || tuplet.normal_notes == 0 {
4011 return Err(Error::InvalidCommand(
4012 "duration scaling requires a non-zero tuplet ratio".into(),
4013 ));
4014 }
4015 if let Some(existing) = &ratio {
4016 if existing != tuplet {
4017 return Err(Error::InvalidCommand(
4018 "duration scaling requires one shared tuplet ratio per voice".into(),
4019 ));
4020 }
4021 } else {
4022 ratio = Some(tuplet.clone());
4023 }
4024 }
4025 Ok(ratio)
4026}
4027
4028fn pad_voice_to_measure_with_tuplet_ratio(
4029 voice: &mut Vec<Note>,
4030 max_beats: f64,
4031 ratio: &TupletInfo,
4032) -> Result<(), Error> {
4033 let mut used: f64 = voice.iter().map(Note::beats).sum();
4034 let ratio_scale = f64::from(ratio.normal_notes) / f64::from(ratio.actual_notes);
4035 while max_beats - used > 1e-9 {
4036 let remaining = max_beats - used;
4037 let duration = [
4038 Duration::Whole,
4039 Duration::Half,
4040 Duration::Quarter,
4041 Duration::Eighth,
4042 Duration::Sixteenth,
4043 Duration::ThirtySecond,
4044 Duration::SixtyFourth,
4045 ]
4046 .into_iter()
4047 .find(|duration| duration.beats(0) * ratio_scale <= remaining + 1e-9)
4048 .ok_or_else(|| {
4049 Error::InvalidCommand(
4050 "duration scaling cannot represent the remaining tuplet duration".into(),
4051 )
4052 })?;
4053 let mut rest = Note::rest(duration);
4054 rest.tuplet = Some(ratio.clone());
4055 used += rest.beats();
4056 voice.push(rest);
4057 }
4058 Ok(())
4059}
4060
4061fn apply_scale_voice_range(cmd: &ScaleVoiceRangeCmd, score: &mut Score) -> Result<(), Error> {
4062 if cmd.voice >= 4 {
4063 return Err(Error::VoiceOutOfRange(cmd.voice));
4064 }
4065 let (start, end) = normalized_structural_measure_range(cmd.start_measure, cmd.end_measure);
4066 let staff = score
4067 .parts
4068 .get(cmd.part_index)
4069 .ok_or(Error::PartNotFound(cmd.part_index))?
4070 .staves
4071 .get(cmd.staff_index)
4072 .ok_or(Error::StaffNotFound(cmd.staff_index))?;
4073 for measure_index in start..=end {
4074 let measure = staff
4075 .measures
4076 .get(measure_index)
4077 .ok_or(Error::MeasureNotFound(measure_index))?;
4078 let expected =
4079 effective_staff_measure_beats(staff, &score.settings.time_signature, measure_index)?;
4080 let voice = &measure.voices[cmd.voice];
4081 match cmd.tuplet_policy {
4082 TupletScalePolicy::PreserveRatio => {
4083 uniform_tuplet_ratio(voice)?;
4084 }
4085 }
4086 let mut scaled_beats = 0.0;
4087 for note in voice {
4088 if scaled_duration(¬e.duration, cmd.scale).is_none() {
4089 return Err(Error::InvalidCommand(
4090 "duration scaling exceeds the portable duration range".into(),
4091 ));
4092 }
4093 if !note.is_grace && !note.is_cue {
4094 scaled_beats += match cmd.scale {
4095 DurationScale::Half => note.beats() / 2.0,
4096 DurationScale::Double => note.beats() * 2.0,
4097 };
4098 }
4099 }
4100 if scaled_beats > expected + 1e-9 {
4101 return Err(Error::InvalidCommand(format!(
4102 "duration scaling overflows measure {measure_index}: {scaled_beats} beats exceeds {expected}"
4103 )));
4104 }
4105 }
4106
4107 for measure_index in start..=end {
4108 let expected = effective_staff_measure_beats(
4109 &score.parts[cmd.part_index].staves[cmd.staff_index],
4110 &score.settings.time_signature,
4111 measure_index,
4112 )?;
4113 let voice = &mut score.parts[cmd.part_index].staves[cmd.staff_index].measures
4114 [measure_index]
4115 .voices[cmd.voice];
4116 if voice.is_empty() {
4117 continue;
4118 }
4119 let tuplet_ratio = match cmd.tuplet_policy {
4120 TupletScalePolicy::PreserveRatio => uniform_tuplet_ratio(voice)?,
4121 };
4122 for note in voice.iter_mut() {
4123 note.duration = scaled_duration(¬e.duration, cmd.scale).ok_or_else(|| {
4124 Error::InvalidCommand("duration scaling exceeds the portable duration range".into())
4125 })?;
4126 }
4127 if let Some(ratio) = tuplet_ratio {
4128 pad_voice_to_measure_with_tuplet_ratio(voice, expected, &ratio)?;
4129 } else {
4130 pad_voice_to_measure(voice, expected);
4131 }
4132 }
4133 Ok(())
4134}
4135
4136fn apply_paste_score_fragment(cmd: &PasteScoreFragmentCmd, score: &mut Score) -> Result<(), Error> {
4137 if !(MIN_SUPPORTED_SCORE_FRAGMENT_CONTRACT_VERSION..=SCORE_FRAGMENT_CONTRACT_VERSION)
4138 .contains(&cmd.fragment.contract_version)
4139 {
4140 return Err(Error::InvalidCommand(format!(
4141 "unsupported score fragment contract version {}",
4142 cmd.fragment.contract_version
4143 )));
4144 }
4145 if cmd.fragment.voices.is_empty() {
4146 return Err(Error::InvalidCommand(
4147 "cannot paste an empty score fragment".into(),
4148 ));
4149 }
4150
4151 let mut lanes = BTreeSet::new();
4155 let mut replaced = BTreeSet::new();
4156 for lane in &cmd.fragment.voices {
4157 let part_index = cmd
4158 .target
4159 .part
4160 .checked_add(lane.relative_part)
4161 .ok_or_else(|| Error::InvalidCommand("fragment part target overflows".into()))?;
4162 let staff_index = cmd
4163 .target
4164 .staff
4165 .checked_add(lane.relative_staff)
4166 .ok_or_else(|| Error::InvalidCommand("fragment staff target overflows".into()))?;
4167 let voice_index = cmd
4168 .target
4169 .voice
4170 .checked_add(lane.relative_voice)
4171 .ok_or_else(|| Error::InvalidCommand("fragment voice target overflows".into()))?;
4172 if voice_index >= 4 {
4173 return Err(Error::VoiceOutOfRange(voice_index));
4174 }
4175 let staff = score
4176 .parts
4177 .get(part_index)
4178 .ok_or(Error::PartNotFound(part_index))?
4179 .staves
4180 .get(staff_index)
4181 .ok_or(Error::StaffNotFound(staff_index))?;
4182 if !lanes.insert((part_index, staff_index, voice_index)) {
4183 return Err(Error::InvalidCommand(
4184 "fragment contains duplicate destination voice lanes".into(),
4185 ));
4186 }
4187 for measure in &lane.measures {
4188 let measure_index = cmd
4189 .target
4190 .measure
4191 .checked_add(measure.relative_measure)
4192 .ok_or_else(|| Error::InvalidCommand("fragment measure target overflows".into()))?;
4193 if measure_index >= staff.measures.len() {
4194 return Err(Error::MeasureNotFound(measure_index));
4195 }
4196 if !measure.cross_staff_targets.is_empty()
4197 && measure.cross_staff_targets.len() != measure.notes.len()
4198 {
4199 return Err(Error::InvalidCommand(
4200 "fragment cross-staff targets do not match note count".into(),
4201 ));
4202 }
4203 if cmd.policy == ScoreFragmentPastePolicy::Merge
4204 && !measure.notes.is_empty()
4205 && staff.measures[measure_index].voices[voice_index]
4206 .iter()
4207 .any(|note| !note.is_rest)
4208 {
4209 return Err(Error::InvalidCommand(
4210 "fragment merge would overwrite a sounding destination lane".into(),
4211 ));
4212 }
4213 if cmd.policy == ScoreFragmentPastePolicy::Merge
4214 && measure.attributes.present
4215 && measure.attributes
4216 != super::fragment::ScoreFragmentMeasureAttributes::from_measure(
4217 &staff.measures[measure_index],
4218 )
4219 {
4220 return Err(Error::InvalidCommand(
4221 "fragment merge would overwrite destination measure attributes".into(),
4222 ));
4223 }
4224 for cross_staff in measure.cross_staff_targets.iter().flatten() {
4225 let target_staff = staff_index as i64 + cross_staff.staff_offset;
4226 if target_staff < 0
4227 || score.parts[part_index]
4228 .staves
4229 .get(target_staff as usize)
4230 .is_none()
4231 {
4232 return Err(Error::InvalidCommand(
4233 "fragment cross-staff target is outside destination part".into(),
4234 ));
4235 }
4236 if cross_staff.target_voice.is_some_and(|voice| voice >= 4) {
4237 return Err(Error::InvalidCommand(
4238 "fragment cross-staff target voice is outside editable range".into(),
4239 ));
4240 }
4241 }
4242 replaced.insert((part_index, staff_index, measure_index, voice_index));
4243 }
4244 }
4245
4246 score.spanners.retain(|spanner| {
4250 !replaced.contains(&(
4251 spanner.start.part,
4252 spanner.start.staff,
4253 spanner.start.measure,
4254 spanner.start.voice,
4255 )) && !replaced.contains(&(
4256 spanner.end.part,
4257 spanner.end.staff,
4258 spanner.end.measure,
4259 spanner.end.voice,
4260 ))
4261 });
4262 for lane in &cmd.fragment.voices {
4263 let part_index = cmd.target.part + lane.relative_part;
4264 let staff_index = cmd.target.staff + lane.relative_staff;
4265 let voice_index = cmd.target.voice + lane.relative_voice;
4266 let staff = &mut score.parts[part_index].staves[staff_index];
4267 for measure in &lane.measures {
4268 let measure_index = cmd.target.measure + measure.relative_measure;
4269 let target = &mut staff.measures[measure_index];
4270 if cmd.policy == ScoreFragmentPastePolicy::Merge && measure.notes.is_empty() {
4271 continue;
4272 }
4273 let mut notes = measure.notes.clone();
4274 for note in &mut notes {
4275 note.id = Uuid::new_v4().to_string();
4276 }
4277 if !measure.cross_staff_targets.is_empty() {
4278 for (note, cross_staff) in notes.iter_mut().zip(&measure.cross_staff_targets) {
4279 note.cross_staff = cross_staff.as_ref().map(|cross_staff| CrossStaff {
4280 target_staff: (staff_index as i64 + cross_staff.staff_offset) as usize,
4281 target_voice: cross_staff.target_voice,
4282 });
4283 }
4284 }
4285 target.voices[voice_index] = notes;
4286 target.source_voice_numbers[voice_index] = measure.source_voice_number;
4287 if cmd.policy == ScoreFragmentPastePolicy::Replace {
4288 measure.attributes.apply_to_measure(target);
4289 }
4290 }
4291 }
4292
4293 let mut used_spanner_ids = score
4294 .spanners
4295 .iter()
4296 .map(|spanner| spanner.id.clone())
4297 .collect::<BTreeSet<_>>();
4298 for source in &cmd.fragment.spanners {
4299 let mut copied = source.clone();
4300 copied.start = fragment_destination_address(&cmd.target, &source.start)?;
4301 copied.end = fragment_destination_address(&cmd.target, &source.end)?;
4302 if note_at(score, &copied.start).is_none() {
4303 return Err(Error::NoteNotFound(copied.start.note));
4304 }
4305 if note_at(score, &copied.end).is_none() {
4306 return Err(Error::NoteNotFound(copied.end.note));
4307 }
4308 copied.id = unique_fragment_spanner_id(&used_spanner_ids, &source.id);
4309 used_spanner_ids.insert(copied.id.clone());
4310 score.spanners.push(copied);
4311 }
4312 Ok(())
4313}
4314
4315fn fragment_destination_address(target: &NoteAddr, relative: &NoteAddr) -> Result<NoteAddr, Error> {
4316 Ok(NoteAddr {
4317 part: target
4318 .part
4319 .checked_add(relative.part)
4320 .ok_or_else(|| Error::InvalidCommand("fragment part target overflows".into()))?,
4321 staff: target
4322 .staff
4323 .checked_add(relative.staff)
4324 .ok_or_else(|| Error::InvalidCommand("fragment staff target overflows".into()))?,
4325 measure: target
4326 .measure
4327 .checked_add(relative.measure)
4328 .ok_or_else(|| Error::InvalidCommand("fragment measure target overflows".into()))?,
4329 voice: target
4330 .voice
4331 .checked_add(relative.voice)
4332 .ok_or_else(|| Error::InvalidCommand("fragment voice target overflows".into()))?,
4333 note: relative.note,
4334 })
4335}
4336
4337fn unique_fragment_spanner_id(used: &BTreeSet<String>, source_id: &str) -> String {
4338 let base = format!("{source_id}-copy");
4339 if !used.contains(&base) {
4340 return base;
4341 }
4342 let mut suffix = 2usize;
4343 loop {
4344 let candidate = format!("{base}-{suffix}");
4345 if !used.contains(&candidate) {
4346 return candidate;
4347 }
4348 suffix += 1;
4349 }
4350}
4351
4352fn apply_toggle_slur(cmd: &ToggleSlurCmd, score: &mut Score) -> Result<(), Error> {
4353 let new_start = !{
4354 score
4355 .parts
4356 .get(cmd.start.part)
4357 .ok_or(Error::PartNotFound(cmd.start.part))?
4358 .staves
4359 .get(cmd.start.staff)
4360 .ok_or(Error::StaffNotFound(cmd.start.staff))?
4361 .measures
4362 .get(cmd.start.measure)
4363 .ok_or(Error::MeasureNotFound(cmd.start.measure))?
4364 .voices
4365 .get(cmd.start.voice)
4366 .ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
4367 .get(cmd.start.note)
4368 .ok_or(Error::NoteNotFound(cmd.start.note))?
4369 .slur_start
4370 };
4371 let new_end = !{
4372 score
4373 .parts
4374 .get(cmd.end.part)
4375 .ok_or(Error::PartNotFound(cmd.end.part))?
4376 .staves
4377 .get(cmd.end.staff)
4378 .ok_or(Error::StaffNotFound(cmd.end.staff))?
4379 .measures
4380 .get(cmd.end.measure)
4381 .ok_or(Error::MeasureNotFound(cmd.end.measure))?
4382 .voices
4383 .get(cmd.end.voice)
4384 .ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
4385 .get(cmd.end.note)
4386 .ok_or(Error::NoteNotFound(cmd.end.note))?
4387 .slur_end
4388 };
4389 score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
4390 [cmd.start.voice][cmd.start.note]
4391 .slur_start = new_start;
4392 score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
4393 [cmd.end.voice][cmd.end.note]
4394 .slur_end = new_end;
4395 Ok(())
4396}
4397
4398fn apply_toggle_trill_line(cmd: &ToggleTrillLineCmd, score: &mut Score) -> Result<(), Error> {
4399 let new_start = !{
4400 score
4401 .parts
4402 .get(cmd.start.part)
4403 .ok_or(Error::PartNotFound(cmd.start.part))?
4404 .staves
4405 .get(cmd.start.staff)
4406 .ok_or(Error::StaffNotFound(cmd.start.staff))?
4407 .measures
4408 .get(cmd.start.measure)
4409 .ok_or(Error::MeasureNotFound(cmd.start.measure))?
4410 .voices
4411 .get(cmd.start.voice)
4412 .ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
4413 .get(cmd.start.note)
4414 .ok_or(Error::NoteNotFound(cmd.start.note))?
4415 .trill_line_start
4416 };
4417 let new_end = !{
4418 score
4419 .parts
4420 .get(cmd.end.part)
4421 .ok_or(Error::PartNotFound(cmd.end.part))?
4422 .staves
4423 .get(cmd.end.staff)
4424 .ok_or(Error::StaffNotFound(cmd.end.staff))?
4425 .measures
4426 .get(cmd.end.measure)
4427 .ok_or(Error::MeasureNotFound(cmd.end.measure))?
4428 .voices
4429 .get(cmd.end.voice)
4430 .ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
4431 .get(cmd.end.note)
4432 .ok_or(Error::NoteNotFound(cmd.end.note))?
4433 .trill_line_end
4434 };
4435 score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
4436 [cmd.start.voice][cmd.start.note]
4437 .trill_line_start = new_start;
4438 score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
4439 [cmd.end.voice][cmd.end.note]
4440 .trill_line_end = new_end;
4441 Ok(())
4442}
4443
4444fn apply_add_staff(cmd: &AddStaffCmd, score: &mut Score) -> Result<(), Error> {
4445 let ts = score.settings.time_signature.clone();
4446 let measure_count = score
4447 .parts
4448 .get(cmd.part_index)
4449 .ok_or(Error::PartNotFound(cmd.part_index))?
4450 .staves
4451 .first()
4452 .map_or(0, |s| s.measures.len());
4453 let mut staff = Staff::new(cmd.clef.clone());
4454 for i in 0..measure_count {
4455 let mut m = Measure::empty(ts.numerator, ts.denominator);
4456 m.number = i as u32 + 1;
4457 staff.measures.push(m);
4458 }
4459 score.parts[cmd.part_index].staves.push(staff);
4460 Ok(())
4461}
4462
4463fn apply_delete_staff(cmd: &DeleteStaffCmd, score: &mut Score) -> Result<(), Error> {
4464 let part = score
4465 .parts
4466 .get_mut(cmd.part_index)
4467 .ok_or(Error::PartNotFound(cmd.part_index))?;
4468 if part.staves.len() <= 1 {
4469 return Err(Error::CannotDeleteLastStaff);
4470 }
4471 if cmd.staff_index >= part.staves.len() {
4472 return Err(Error::StaffNotFound(cmd.staff_index));
4473 }
4474 part.staves.remove(cmd.staff_index);
4475 remap_spanners(score, |address| {
4476 if address.part != cmd.part_index {
4477 Some(address.clone())
4478 } else if address.staff == cmd.staff_index {
4479 None
4480 } else if address.staff > cmd.staff_index {
4481 let mut shifted = address.clone();
4482 shifted.staff -= 1;
4483 Some(shifted)
4484 } else {
4485 Some(address.clone())
4486 }
4487 });
4488 Ok(())
4489}
4490
4491fn same_voice(address: &NoteAddr, part: usize, staff: usize, measure: usize, voice: usize) -> bool {
4493 address.part == part
4494 && address.staff == staff
4495 && address.measure == measure
4496 && address.voice == voice
4497}
4498
4499fn trim_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
4500 let mut total = 0.0f64;
4501 let mut cutoff = voice.len();
4502 for (i, n) in voice.iter().enumerate() {
4503 total += n.beats();
4504 if total > max_beats + 1e-9 {
4505 cutoff = i;
4506 break;
4507 }
4508 }
4509 voice.truncate(cutoff);
4510 pad_voice_to_measure(voice, max_beats);
4511}
4512
4513fn pad_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
4514 let mut used: f64 = voice.iter().map(|n| n.beats()).sum();
4515 while max_beats - used > 1e-9 {
4516 let remaining = max_beats - used;
4517 let rest = Note::rest(Duration::whole_filling_beats(remaining));
4518 used += rest.beats();
4519 voice.push(rest);
4520 }
4521}
4522
4523#[cfg(test)]
4524mod tests {
4525 use super::*;
4526 use crate::ScoreEngine;
4527 use crate::model::pitch::Step;
4528
4529 fn default_engine_score() -> Score {
4530 let mut s = Score::default();
4531 for part in &mut s.parts {
4532 for staff in &mut part.staves {
4533 for (i, m) in staff.measures.iter_mut().enumerate() {
4534 m.number = i as u32 + 1;
4535 }
4536 }
4537 }
4538 s
4539 }
4540
4541 #[test]
4542 fn legacy_scale_voice_range_command_defaults_to_preserve_tuplet_ratio() {
4543 let command: Command = serde_json::from_str(
4544 r#"{"type":"scale_voice_range","part_index":0,"staff_index":0,"voice":0,"start_measure":0,"end_measure":0,"scale":"half"}"#,
4545 )
4546 .expect("legacy scale command deserializes");
4547 let Command::ScaleVoiceRange(command) = command else {
4548 panic!("expected scale command");
4549 };
4550 assert_eq!(command.tuplet_policy, TupletScalePolicy::PreserveRatio);
4551 }
4552
4553 fn score_with_typed_spanner() -> Score {
4554 use crate::model::score::{NotationSpanner, NotationSpannerKind};
4555
4556 let mut score = Score::new("Spanner edits", 120, 4, 4, 0, 1);
4557 score.parts[0].staves[0].measures[0].voices[0] = (0..4)
4558 .map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
4559 .collect();
4560 score.spanners.push(NotationSpanner {
4561 id: "typed-glissando".to_string(),
4562 kind: NotationSpannerKind::Glissando,
4563 start: NoteAddr {
4564 part: 0,
4565 staff: 0,
4566 measure: 0,
4567 voice: 0,
4568 note: 1,
4569 },
4570 end: NoteAddr {
4571 part: 0,
4572 staff: 0,
4573 measure: 0,
4574 voice: 0,
4575 note: 2,
4576 },
4577 number: Some(1),
4578 line_type: None,
4579 text: None,
4580 placement: None,
4581 ottava_size: None,
4582 ottava_type: None,
4583 });
4584 score
4585 }
4586
4587 fn insert_note_at(position: usize) -> Command {
4588 Command::AddNote(AddNoteCmd {
4589 part_index: 0,
4590 staff_index: 0,
4591 measure_index: 0,
4592 voice: 0,
4593 position,
4594 pitch: Some(Pitch::new(Step::D, 5)),
4595 duration: Duration::Quarter,
4596 dot_count: 0,
4597 is_rest: false,
4598 tuplet: None,
4599 })
4600 }
4601
4602 #[test]
4603 fn add_note_inserts_into_voice() {
4604 let mut score = default_engine_score();
4605 let cmd = Command::AddNote(AddNoteCmd {
4606 part_index: 0,
4607 staff_index: 0,
4608 measure_index: 0,
4609 voice: 0,
4610 position: 0,
4611 pitch: Some(Pitch::new(Step::C, 4)),
4612 duration: Duration::Quarter,
4613 dot_count: 0,
4614 is_rest: false,
4615 tuplet: None,
4616 });
4617 apply_command(&cmd, &mut score).unwrap();
4618 let first = &score.parts[0].staves[0].measures[0].voices[0][0];
4619 assert!(!first.is_rest);
4620 assert_eq!(first.pitches[0].step, Step::C);
4621 }
4622
4623 #[test]
4624 fn typed_spanners_follow_note_insertions_at_every_relative_position() {
4625 for (position, expected_start, expected_end) in [
4626 (0, 2, 3), (1, 2, 3), (2, 1, 3), (4, 1, 2), (3, 1, 2), ] {
4632 let mut score = score_with_typed_spanner();
4633 apply_command(&insert_note_at(position), &mut score).unwrap();
4634 let span = score.spanners.first().expect("span remains attached");
4635 assert_eq!(span.start.note, expected_start, "position {position}");
4636 assert_eq!(span.end.note, expected_end, "position {position}");
4637 }
4638 }
4639
4640 #[test]
4641 fn deleting_a_typed_spanner_endpoint_removes_the_whole_span_and_undo_redo_is_atomic() {
4642 let mut score = score_with_typed_spanner();
4643 let endpoint_id = score.parts[0].staves[0].measures[0].voices[0][1].id.clone();
4644 let mut stack = CommandStack::new(8);
4645
4646 stack
4647 .execute(
4648 Command::DeleteNote(DeleteNoteCmd {
4649 note_id: endpoint_id,
4650 part_index: 0,
4651 staff_index: 0,
4652 measure_index: 0,
4653 voice: 0,
4654 }),
4655 &mut score,
4656 )
4657 .unwrap();
4658 assert!(score.spanners.is_empty());
4659
4660 stack.undo(&mut score).unwrap();
4661 assert_eq!(score.spanners[0].start.note, 1);
4662 assert_eq!(score.spanners[0].end.note, 2);
4663
4664 stack.redo(&mut score).unwrap();
4665 assert!(score.spanners.is_empty());
4666 }
4667
4668 #[test]
4669 fn batched_structural_edits_remap_typed_spanners_and_restore_them_on_undo() {
4670 let mut score = score_with_typed_spanner();
4671 let mut stack = CommandStack::new(8);
4672 stack
4673 .batch_execute(
4674 vec![
4675 insert_note_at(0),
4676 Command::SetTempo(SetTempoCmd { bpm: 144 }),
4677 ],
4678 &mut score,
4679 )
4680 .unwrap();
4681 assert_eq!(score.spanners[0].start.note, 2);
4682 assert_eq!(score.spanners[0].end.note, 3);
4683
4684 stack.undo(&mut score).unwrap();
4685 assert_eq!(score.spanners[0].start.note, 1);
4686 assert_eq!(score.spanners[0].end.note, 2);
4687
4688 stack.redo(&mut score).unwrap();
4689 assert_eq!(score.spanners[0].start.note, 2);
4690 assert_eq!(score.spanners[0].end.note, 3);
4691 }
4692
4693 #[test]
4694 fn typed_spanners_remap_only_the_edited_endpoint_across_staff_and_voice() {
4695 use crate::model::score::{NotationSpanner, NotationSpannerKind, ScoreTemplate};
4696
4697 let mut score = Score::template(ScoreTemplate::Piano);
4698 for staff in &mut score.parts[0].staves {
4699 let notes: Vec<Note> = (0..4)
4700 .map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
4701 .collect();
4702 staff.measures[0].voices[0] = notes.clone();
4703 staff.measures[0].voices[1] = notes;
4704 }
4705 score.spanners.push(NotationSpanner {
4706 id: "cross-staff".to_string(),
4707 kind: NotationSpannerKind::Slur,
4708 start: NoteAddr {
4709 part: 0,
4710 staff: 0,
4711 measure: 0,
4712 voice: 0,
4713 note: 1,
4714 },
4715 end: NoteAddr {
4716 part: 0,
4717 staff: 1,
4718 measure: 0,
4719 voice: 1,
4720 note: 2,
4721 },
4722 number: Some(2),
4723 line_type: None,
4724 text: None,
4725 placement: None,
4726 ottava_size: None,
4727 ottava_type: None,
4728 });
4729
4730 let mut stack = CommandStack::new(8);
4731 stack.execute(insert_note_at(0), &mut score).unwrap();
4732 let span = &score.spanners[0];
4733 assert_eq!(span.start.note, 2);
4734 assert_eq!(span.end.staff, 1);
4735 assert_eq!(span.end.voice, 1);
4736 assert_eq!(span.end.note, 2);
4737 }
4738
4739 #[test]
4740 fn set_fingerings_keeps_first_legacy_value_in_sync() {
4741 let mut score = default_engine_score();
4742 apply_command(
4743 &Command::AddNote(AddNoteCmd {
4744 part_index: 0,
4745 staff_index: 0,
4746 measure_index: 0,
4747 voice: 0,
4748 position: 0,
4749 pitch: Some(Pitch::new(Step::C, 4)),
4750 duration: Duration::Quarter,
4751 dot_count: 0,
4752 is_rest: false,
4753 tuplet: None,
4754 }),
4755 &mut score,
4756 )
4757 .unwrap();
4758 apply_command(
4759 &Command::SetFingerings(SetFingeringsCmd {
4760 part_index: 0,
4761 staff_index: 0,
4762 measure_index: 0,
4763 voice: 0,
4764 note_index: 0,
4765 fingerings: vec![1, 3, 4],
4766 }),
4767 &mut score,
4768 )
4769 .unwrap();
4770 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
4771 assert_eq!(note.fingerings, vec![1, 3, 4]);
4772 assert_eq!(note.fingering, Some(1));
4773 }
4774
4775 #[test]
4776 fn set_figured_bass_replaces_measure_figures() {
4777 let mut score = default_engine_score();
4778 let figures = vec![FiguredBassFigure {
4779 number: "6".to_string(),
4780 alter: Some("-1".to_string()),
4781 prefix: Some("+".to_string()),
4782 suffix: None,
4783 extender: false,
4784 }];
4785 apply_command(
4786 &Command::SetFiguredBass(SetFiguredBassCmd {
4787 measure_index: 0,
4788 figures: figures.clone(),
4789 }),
4790 &mut score,
4791 )
4792 .unwrap();
4793 assert_eq!(score.parts[0].staves[0].measures[0].figured_bass, figures);
4794 }
4795
4796 #[test]
4797 fn set_measure_text_supports_append_replace_remove_and_undo_redo() {
4798 let mut engine = crate::ScoreEngine::new();
4799 let address = SetMeasureTextCmd {
4800 part_index: 0,
4801 staff_index: 0,
4802 measure_index: 0,
4803 text_index: 0,
4804 text: Some(StyledText {
4805 style: crate::TextStyle::Technique,
4806 text: "dolce".to_string(),
4807 placement: None,
4808 offset_x: None,
4809 offset_y: None,
4810 relative_x: None,
4811 relative_y: None,
4812 }),
4813 };
4814 engine.apply(Command::SetMeasureText(address)).unwrap();
4815 assert_eq!(
4816 engine.score.parts[0].staves[0].measures[0].texts[0],
4817 StyledText {
4818 style: crate::TextStyle::Technique,
4819 text: "dolce".to_string(),
4820 placement: None,
4821 offset_x: None,
4822 offset_y: None,
4823 relative_x: None,
4824 relative_y: None,
4825 }
4826 );
4827
4828 engine
4829 .apply(Command::SetMeasureText(SetMeasureTextCmd {
4830 text_index: 0,
4831 text: Some(StyledText {
4832 style: crate::TextStyle::RehearsalMark,
4833 text: "A".to_string(),
4834 placement: None,
4835 offset_x: None,
4836 offset_y: None,
4837 relative_x: None,
4838 relative_y: None,
4839 }),
4840 ..SetMeasureTextCmd {
4841 part_index: 0,
4842 staff_index: 0,
4843 measure_index: 0,
4844 text_index: 0,
4845 text: None,
4846 }
4847 }))
4848 .unwrap();
4849 assert_eq!(
4850 engine.score.parts[0].staves[0].measures[0].texts[0].style,
4851 crate::TextStyle::RehearsalMark
4852 );
4853
4854 engine
4855 .apply(Command::SetMeasureText(SetMeasureTextCmd {
4856 text_index: 0,
4857 text: None,
4858 ..SetMeasureTextCmd {
4859 part_index: 0,
4860 staff_index: 0,
4861 measure_index: 0,
4862 text_index: 0,
4863 text: None,
4864 }
4865 }))
4866 .unwrap();
4867 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
4868 engine.undo().unwrap();
4869 assert_eq!(engine.score.parts[0].staves[0].measures[0].texts.len(), 1);
4870 engine.redo().unwrap();
4871 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
4872 }
4873
4874 #[test]
4875 fn set_measure_text_rejects_invalid_index_atomically_and_round_trips_json() {
4876 let mut score = default_engine_score();
4877 let before = score.clone();
4878 let command = Command::SetMeasureText(SetMeasureTextCmd {
4879 part_index: 0,
4880 staff_index: 0,
4881 measure_index: 0,
4882 text_index: 2,
4883 text: Some(StyledText {
4884 style: crate::TextStyle::Expression,
4885 text: "espressivo".to_string(),
4886 placement: None,
4887 offset_x: None,
4888 offset_y: None,
4889 relative_x: None,
4890 relative_y: None,
4891 }),
4892 });
4893 let json = serde_json::to_string(&command).unwrap();
4894 let restored: Command = serde_json::from_str(&json).unwrap();
4895 assert_eq!(
4896 serde_json::to_value(&restored).unwrap(),
4897 serde_json::to_value(&command).unwrap()
4898 );
4899 assert!(apply_command(&restored, &mut score).is_err());
4900 assert_eq!(
4901 serde_json::to_value(&score).unwrap(),
4902 serde_json::to_value(&before).unwrap()
4903 );
4904 }
4905
4906 #[test]
4907 fn set_score_text_supports_append_replace_remove_and_undo_redo() {
4908 let mut engine = crate::ScoreEngine::new();
4909 let text = StyledText {
4910 style: crate::TextStyle::Expression,
4911 text: "Title".to_string(),
4912 placement: None,
4913 offset_x: None,
4914 offset_y: None,
4915 relative_x: None,
4916 relative_y: None,
4917 };
4918 engine
4919 .apply(Command::SetScoreText(SetScoreTextCmd {
4920 text_index: 0,
4921 text: Some(text.clone()),
4922 }))
4923 .unwrap();
4924 assert_eq!(engine.score.texts, vec![text.clone()]);
4925
4926 let mut replacement = text.clone();
4927 replacement.text = "Subtitle".to_string();
4928 engine
4929 .apply(Command::SetScoreText(SetScoreTextCmd {
4930 text_index: 0,
4931 text: Some(replacement.clone()),
4932 }))
4933 .unwrap();
4934 assert_eq!(engine.score.texts, vec![replacement]);
4935
4936 engine
4937 .apply(Command::SetScoreText(SetScoreTextCmd {
4938 text_index: 0,
4939 text: None,
4940 }))
4941 .unwrap();
4942 assert!(engine.score.texts.is_empty());
4943 engine.undo().unwrap();
4944 assert_eq!(engine.score.texts.len(), 1);
4945 engine.redo().unwrap();
4946 assert!(engine.score.texts.is_empty());
4947 }
4948
4949 #[test]
4950 fn set_score_style_overrides_is_undoable_and_rejects_invalid_values() {
4951 let mut engine = crate::ScoreEngine::new();
4952 let overrides = vec![ViewStyleOverride {
4953 property: super::super::score::ViewStyleProperty::SystemGap,
4954 value: 2.5,
4955 }];
4956 engine
4957 .apply(Command::SetScoreStyleOverrides(SetScoreStyleOverridesCmd {
4958 overrides: overrides.clone(),
4959 }))
4960 .unwrap();
4961 assert_eq!(engine.score.style_overrides, overrides);
4962 engine.undo().unwrap();
4963 assert!(engine.score.style_overrides.is_empty());
4964 engine.redo().unwrap();
4965 assert_eq!(engine.score.style_overrides, overrides);
4966
4967 let before = engine.score.clone();
4968 assert!(
4969 engine
4970 .apply(Command::SetScoreStyleOverrides(SetScoreStyleOverridesCmd {
4971 overrides: vec![ViewStyleOverride {
4972 property: super::super::score::ViewStyleProperty::TextScale,
4973 value: f32::NAN,
4974 }],
4975 }))
4976 .is_err()
4977 );
4978 assert_eq!(
4979 serde_json::to_value(&engine.score).unwrap(),
4980 serde_json::to_value(&before).unwrap()
4981 );
4982 }
4983
4984 #[test]
4985 fn set_object_style_overrides_is_undoable_and_requires_existing_target() {
4986 let mut engine = crate::ScoreEngine::new();
4987 engine.score.texts.push(StyledText {
4988 style: crate::TextStyle::Expression,
4989 text: "Allegro".into(),
4990 placement: None,
4991 offset_x: None,
4992 offset_y: None,
4993 relative_x: None,
4994 relative_y: None,
4995 });
4996 let overrides = vec![ObjectStyleOverride {
4997 target: super::super::score::ObjectStyleTarget::ScoreText { text_index: 0 },
4998 property: super::super::score::ViewStyleProperty::TextScale,
4999 value: 1.2,
5000 provenance: None,
5001 }];
5002 engine
5003 .apply(Command::SetObjectStyleOverrides(
5004 SetObjectStyleOverridesCmd {
5005 overrides: overrides.clone(),
5006 },
5007 ))
5008 .unwrap();
5009 assert_eq!(engine.score.object_style_overrides, overrides);
5010 engine.undo().unwrap();
5011 assert!(engine.score.object_style_overrides.is_empty());
5012
5013 assert!(
5014 engine
5015 .apply(Command::SetObjectStyleOverrides(
5016 SetObjectStyleOverridesCmd {
5017 overrides: vec![ObjectStyleOverride {
5018 target: super::super::score::ObjectStyleTarget::ScoreText {
5019 text_index: 1,
5020 },
5021 property: super::super::score::ViewStyleProperty::TextScale,
5022 value: 1.2,
5023 provenance: None,
5024 }],
5025 }
5026 ))
5027 .is_err()
5028 );
5029 }
5030
5031 #[test]
5032 fn set_harmony_range_is_undoable_and_json_compatible() {
5033 let mut score = default_engine_score();
5034 score.parts[0].staves[0].measures[0].voices[0] =
5035 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
5036 score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
5037 root: "C".to_owned(),
5038 kind: "major".to_owned(),
5039 bass: None,
5040 placement: None,
5041 extender: true,
5042 harmonic_degree: None,
5043 harmony_function: None,
5044 harmony_type: None,
5045 chord_ref: None,
5046 range_end: None,
5047 degrees: Vec::new(),
5048 });
5049 let mut engine = ScoreEngine::new();
5050 engine.replace_score(score);
5051 let command = Command::SetHarmonyRange(SetHarmonyRangeCmd {
5052 part_index: 0,
5053 staff_index: 0,
5054 measure_index: 0,
5055 voice: 0,
5056 note_index: 0,
5057 end: Some(NoteAddr {
5058 part: 0,
5059 staff: 0,
5060 measure: 0,
5061 voice: 0,
5062 note: 0,
5063 }),
5064 });
5065 let json = serde_json::to_string(&command).unwrap();
5066 let restored: Command = serde_json::from_str(&json).unwrap();
5067 engine.apply(restored).unwrap();
5068 assert!(
5069 engine.score.parts[0].staves[0].measures[0].voices[0][0]
5070 .chord_symbol
5071 .as_ref()
5072 .and_then(|chord| chord.range_end.as_ref())
5073 .is_some()
5074 );
5075 engine.undo().unwrap();
5076 assert!(
5077 engine.score.parts[0].staves[0].measures[0].voices[0][0]
5078 .chord_symbol
5079 .as_ref()
5080 .is_some_and(|chord| chord.range_end.is_none())
5081 );
5082 }
5083
5084 #[test]
5085 fn set_harp_pedal_diagrams_is_undoable_and_json_compatible() {
5086 let mut engine = ScoreEngine::new();
5087 let mut diagram = HarpPedalDiagram::default();
5088 diagram.positions[0] = super::super::score::HarpPedalPosition::Flat;
5089 diagram.positions[6] = super::super::score::HarpPedalPosition::Sharp;
5090 let command = Command::SetHarpPedalDiagrams(SetHarpPedalDiagramsCmd {
5091 part_index: 0,
5092 staff_index: 0,
5093 measure_index: 0,
5094 diagrams: vec![diagram.clone()],
5095 });
5096 let restored: Command =
5097 serde_json::from_str(&serde_json::to_string(&command).unwrap()).unwrap();
5098 engine.apply(restored).unwrap();
5099 assert_eq!(
5100 engine.score.parts[0].staves[0].measures[0].harp_pedal_diagrams,
5101 vec![diagram]
5102 );
5103 engine.undo().unwrap();
5104 assert!(
5105 engine.score.parts[0].staves[0].measures[0]
5106 .harp_pedal_diagrams
5107 .is_empty()
5108 );
5109 }
5110
5111 #[test]
5112 fn set_note_placement_is_undoable_and_rejects_non_finite_values() {
5113 let mut score = default_engine_score();
5114 score.parts[0].staves[0].measures[0].voices[0] =
5115 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5116 let mut engine = ScoreEngine::new();
5117 engine.replace_score(score);
5118 let command = Command::SetNotePlacement(SetNotePlacementCmd {
5119 part_index: 0,
5120 staff_index: 0,
5121 measure_index: 0,
5122 voice: 0,
5123 note_index: 0,
5124 offset_x: Some(12.5),
5125 offset_y: Some(-3.0),
5126 relative_x: Some(1.25),
5127 relative_y: Some(-0.5),
5128 });
5129 engine.apply(command).unwrap();
5130 let note = &engine.score.parts[0].staves[0].measures[0].voices[0][0];
5131 assert_eq!(note.offset_x, Some(12.5));
5132 assert_eq!(note.relative_y, Some(-0.5));
5133 engine.undo().unwrap();
5134 assert_eq!(
5135 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
5136 None
5137 );
5138 let invalid = Command::SetNotePlacement(SetNotePlacementCmd {
5139 part_index: 0,
5140 staff_index: 0,
5141 measure_index: 0,
5142 voice: 0,
5143 note_index: 0,
5144 offset_x: Some(f64::NAN),
5145 offset_y: None,
5146 relative_x: None,
5147 relative_y: None,
5148 });
5149 assert!(engine.apply(invalid).is_err());
5150 assert_eq!(
5151 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
5152 None
5153 );
5154 }
5155
5156 #[test]
5157 fn set_guitar_bend_alter_updates_note_and_can_clear() {
5158 let mut score = default_engine_score();
5159 apply_command(
5160 &Command::AddNote(AddNoteCmd {
5161 part_index: 0,
5162 staff_index: 0,
5163 measure_index: 0,
5164 voice: 0,
5165 position: 0,
5166 pitch: Some(Pitch::new(Step::G, 4)),
5167 duration: Duration::Quarter,
5168 dot_count: 0,
5169 is_rest: false,
5170 tuplet: None,
5171 }),
5172 &mut score,
5173 )
5174 .unwrap();
5175 let location = SetGuitarBendAlterCmd {
5176 part_index: 0,
5177 staff_index: 0,
5178 measure_index: 0,
5179 voice: 0,
5180 note_index: 0,
5181 alter_cents: Some(200),
5182 };
5183 apply_command(&Command::SetGuitarBendAlter(location.clone()), &mut score).unwrap();
5184 assert_eq!(
5185 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
5186 Some(200)
5187 );
5188 let mut cleared = location;
5189 cleared.alter_cents = None;
5190 apply_command(&Command::SetGuitarBendAlter(cleared), &mut score).unwrap();
5191 assert_eq!(
5192 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
5193 None
5194 );
5195 }
5196
5197 #[test]
5198 fn set_guitar_bend_curve_is_undoable_and_json_compatible() {
5199 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5200 let command = Command::SetGuitarBendCurve(SetGuitarBendCurveCmd {
5201 part_index: 0,
5202 staff_index: 0,
5203 measure_index: 0,
5204 voice: 0,
5205 note_index: 0,
5206 points: vec![
5207 crate::GuitarBendPoint {
5208 position_per_mille: 0,
5209 alter_cents: 0,
5210 },
5211 crate::GuitarBendPoint {
5212 position_per_mille: 500,
5213 alter_cents: 200,
5214 },
5215 crate::GuitarBendPoint {
5216 position_per_mille: 1000,
5217 alter_cents: 0,
5218 },
5219 ],
5220 });
5221 let decoded: Command =
5222 serde_json::from_str(&serde_json::to_string(&command).unwrap()).unwrap();
5223 let mut stack = CommandStack::new(16);
5224 stack.execute(decoded, &mut score).unwrap();
5225 assert_eq!(
5226 score.parts[0].staves[0].measures[0].voices[0][0]
5227 .guitar_bend_curve
5228 .len(),
5229 3
5230 );
5231 stack.undo(&mut score).unwrap();
5232 assert!(
5233 score.parts[0].staves[0].measures[0].voices[0][0]
5234 .guitar_bend_curve
5235 .is_empty()
5236 );
5237 }
5238
5239 #[test]
5240 fn set_duration_updates_note_and_preserves_measure_capacity() {
5241 let mut score = default_engine_score();
5242 apply_command(
5243 &Command::AddNote(AddNoteCmd {
5244 part_index: 0,
5245 staff_index: 0,
5246 measure_index: 0,
5247 voice: 0,
5248 position: 0,
5249 pitch: Some(Pitch::new(Step::C, 4)),
5250 duration: Duration::Quarter,
5251 dot_count: 0,
5252 is_rest: false,
5253 tuplet: None,
5254 }),
5255 &mut score,
5256 )
5257 .unwrap();
5258 apply_command(
5259 &Command::SetDuration(SetDurationCmd {
5260 part_index: 0,
5261 staff_index: 0,
5262 measure_index: 0,
5263 voice: 0,
5264 note_index: 0,
5265 duration: Duration::Half,
5266 dot_count: 1,
5267 }),
5268 &mut score,
5269 )
5270 .unwrap();
5271 let voice = &score.parts[0].staves[0].measures[0].voices[0];
5272 assert_eq!(voice[0].duration, Duration::Half);
5273 assert_eq!(voice[0].dot_count, 1);
5274 assert!((voice.iter().map(|note| note.beats()).sum::<f64>() - 4.0).abs() < 1e-9);
5275 }
5276
5277 #[test]
5278 fn set_tempo_updates_score() {
5279 let mut score = default_engine_score();
5280 let cmd = Command::SetTempo(SetTempoCmd { bpm: 160 });
5281 apply_command(&cmd, &mut score).unwrap();
5282 assert_eq!(score.settings.tempo_bpm, 160);
5283 }
5284
5285 #[test]
5286 fn add_measure_increases_count() {
5287 let mut score = default_engine_score();
5288 let before = score.measure_count();
5289 apply_command(
5290 &Command::AddMeasure(AddMeasureCmd { after_index: 0 }),
5291 &mut score,
5292 )
5293 .unwrap();
5294 assert_eq!(score.measure_count(), before + 1);
5295 }
5296
5297 #[test]
5298 fn delete_measure_decreases_count() {
5299 let mut score = default_engine_score();
5300 let before = score.measure_count();
5301 apply_command(
5302 &Command::DeleteMeasure(DeleteMeasureCmd { measure_index: 0 }),
5303 &mut score,
5304 )
5305 .unwrap();
5306 assert_eq!(score.measure_count(), before - 1);
5307 }
5308
5309 #[test]
5310 fn undo_restores_score() {
5311 let mut stack = CommandStack::new(50);
5312 let mut score = default_engine_score();
5313 let before = score.settings.tempo_bpm;
5314 stack
5315 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
5316 .unwrap();
5317 assert_eq!(score.settings.tempo_bpm, 200);
5318 stack.undo(&mut score).unwrap();
5319 assert_eq!(score.settings.tempo_bpm, before);
5320 }
5321
5322 #[test]
5323 fn redo_reapplies_command() {
5324 let mut stack = CommandStack::new(50);
5325 let mut score = default_engine_score();
5326 stack
5327 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
5328 .unwrap();
5329 stack.undo(&mut score).unwrap();
5330 stack.redo(&mut score).unwrap();
5331 assert_eq!(score.settings.tempo_bpm, 200);
5332 }
5333
5334 #[test]
5335 fn undo_nothing_returns_error() {
5336 let mut stack = CommandStack::new(50);
5337 let mut score = default_engine_score();
5338 assert!(stack.undo(&mut score).is_err());
5339 }
5340
5341 #[test]
5342 fn add_part_appends_part() {
5343 let mut score = default_engine_score();
5344 let before = score.parts.len();
5345 apply_command(
5346 &Command::AddPart(AddPartCmd {
5347 name: "Violin".into(),
5348 short_name: "Vln.".into(),
5349 clefs: vec!["Treble".into()],
5350 midi_channel: 0,
5351 midi_program: 0,
5352 }),
5353 &mut score,
5354 )
5355 .unwrap();
5356 assert_eq!(score.parts.len(), before + 1);
5357 }
5358
5359 #[test]
5360 fn delete_part_removes_part() {
5361 let mut score = default_engine_score();
5362 apply_command(
5363 &Command::AddPart(AddPartCmd {
5364 name: "Violin".into(),
5365 short_name: "V.".into(),
5366 clefs: vec!["Treble".into()],
5367 midi_channel: 0,
5368 midi_program: 0,
5369 }),
5370 &mut score,
5371 )
5372 .unwrap();
5373 let before = score.parts.len();
5374 apply_command(
5375 &Command::DeletePart(DeletePartCmd { part_index: 0 }),
5376 &mut score,
5377 )
5378 .unwrap();
5379 assert_eq!(score.parts.len(), before - 1);
5380 }
5381
5382 #[test]
5383 fn delete_part_out_of_range_returns_err() {
5384 let mut score = default_engine_score();
5385 assert!(
5386 apply_command(
5387 &Command::DeletePart(DeletePartCmd { part_index: 99 }),
5388 &mut score
5389 )
5390 .is_err()
5391 );
5392 }
5393
5394 #[test]
5395 fn delete_part_undo_restores_part() {
5396 let mut stack = CommandStack::new(50);
5397 let mut score = default_engine_score();
5398 apply_command(
5399 &Command::AddPart(AddPartCmd {
5400 name: "Violin".into(),
5401 short_name: "V.".into(),
5402 clefs: vec!["Treble".into()],
5403 midi_channel: 0,
5404 midi_program: 0,
5405 }),
5406 &mut score,
5407 )
5408 .unwrap();
5409 let before = score.parts.len();
5410 stack
5411 .execute(
5412 Command::DeletePart(DeletePartCmd { part_index: 0 }),
5413 &mut score,
5414 )
5415 .unwrap();
5416 assert_eq!(score.parts.len(), before - 1);
5417 stack.undo(&mut score).unwrap();
5418 assert_eq!(score.parts.len(), before);
5419 }
5420
5421 #[test]
5422 fn reorder_parts_remaps_linked_views_and_preserves_undo() {
5423 let mut engine = crate::ScoreEngine::new();
5424 engine
5425 .apply(Command::AddPart(AddPartCmd {
5426 name: "Flute".into(),
5427 short_name: "Fl.".into(),
5428 clefs: vec!["Treble".into()],
5429 midi_channel: 1,
5430 midi_program: 73,
5431 }))
5432 .unwrap();
5433 engine
5434 .score
5435 .views
5436 .push(ScoreView::linked_part("flute", "Flute", 1));
5437 engine.score.part_groups.push(PartGroup {
5438 first_part: 0,
5439 last_part: 1,
5440 symbol: super::super::score::PartGroupSymbol::Bracket,
5441 barlines_connect: false,
5442 });
5443
5444 engine
5445 .apply(Command::ReorderParts(ReorderPartsCmd { order: vec![1, 0] }))
5446 .unwrap();
5447 assert_eq!(engine.score.parts[0].name, "Flute");
5448 assert_eq!(engine.score.views[0].parts, vec![0]);
5449 engine.undo().unwrap();
5450 assert_eq!(engine.score.parts[0].name, "Piano");
5451 assert_eq!(engine.score.views[0].parts, vec![1]);
5452 }
5453
5454 #[test]
5455 fn reorder_parts_rejects_splitting_a_part_group_without_mutation() {
5456 let mut score = Score::template(ScoreTemplate::StringQuartet);
5457 score.part_groups.push(PartGroup {
5458 first_part: 0,
5459 last_part: 1,
5460 symbol: super::super::score::PartGroupSymbol::Bracket,
5461 barlines_connect: false,
5462 });
5463 let before = serde_json::to_value(&score).unwrap();
5464
5465 assert!(
5466 apply_command(
5467 &Command::ReorderParts(ReorderPartsCmd {
5468 order: vec![0, 2, 1, 3],
5469 }),
5470 &mut score,
5471 )
5472 .is_err()
5473 );
5474 assert_eq!(serde_json::to_value(&score).unwrap(), before);
5475 }
5476
5477 #[test]
5478 fn set_metadata_updates_title() {
5479 let mut score = default_engine_score();
5480 apply_command(
5481 &Command::SetMetadata(SetMetadataCmd {
5482 title: Some("New Title".into()),
5483 ..Default::default()
5484 }),
5485 &mut score,
5486 )
5487 .unwrap();
5488 assert_eq!(score.metadata.title, "New Title");
5489 }
5490
5491 #[test]
5492 fn set_metadata_none_fields_skipped() {
5493 let mut score = default_engine_score();
5494 let original_composer = score.metadata.composer.clone();
5495 apply_command(
5496 &Command::SetMetadata(SetMetadataCmd {
5497 title: Some("X".into()),
5498 ..Default::default()
5499 }),
5500 &mut score,
5501 )
5502 .unwrap();
5503 assert_eq!(score.metadata.composer, original_composer);
5504 }
5505
5506 #[test]
5507 fn set_volta_sets_bracket() {
5508 use crate::model::score::VoltaBracket;
5509 let mut score = default_engine_score();
5510 let volta = VoltaBracket {
5511 number: 1,
5512 kind: "begin_end".into(),
5513 };
5514 apply_command(
5515 &Command::SetVolta(SetVoltaCmd {
5516 measure_index: 0,
5517 volta: Some(volta.clone()),
5518 }),
5519 &mut score,
5520 )
5521 .unwrap();
5522 assert!(score.parts[0].staves[0].measures[0].volta.is_some());
5523 }
5524
5525 #[test]
5526 fn set_volta_none_clears_bracket() {
5527 use crate::model::score::VoltaBracket;
5528 let mut score = default_engine_score();
5529 score.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
5530 number: 1,
5531 kind: "begin_end".into(),
5532 });
5533 apply_command(
5534 &Command::SetVolta(SetVoltaCmd {
5535 measure_index: 0,
5536 volta: None,
5537 }),
5538 &mut score,
5539 )
5540 .unwrap();
5541 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
5542 }
5543
5544 #[test]
5545 fn set_volta_undo_restores_old() {
5546 use crate::model::score::VoltaBracket;
5547 let mut stack = CommandStack::new(50);
5548 let mut score = default_engine_score();
5549 stack
5550 .execute(
5551 Command::SetVolta(SetVoltaCmd {
5552 measure_index: 0,
5553 volta: Some(VoltaBracket {
5554 number: 1,
5555 kind: "begin_end".into(),
5556 }),
5557 }),
5558 &mut score,
5559 )
5560 .unwrap();
5561 stack.undo(&mut score).unwrap();
5562 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
5563 }
5564
5565 #[test]
5566 fn set_clef_updates_staff_clef() {
5567 use crate::model::notation::Clef;
5568 let mut score = default_engine_score();
5569 apply_command(
5570 &Command::SetClef(SetClefCmd {
5571 part_index: 0,
5572 staff_index: 0,
5573 clef: Clef::Bass,
5574 }),
5575 &mut score,
5576 )
5577 .unwrap();
5578 assert_eq!(score.parts[0].staves[0].clef, Clef::Bass);
5579 }
5580
5581 #[test]
5582 fn set_clef_out_of_range_returns_err() {
5583 use crate::model::notation::Clef;
5584 let mut score = default_engine_score();
5585 assert!(
5586 apply_command(
5587 &Command::SetClef(SetClefCmd {
5588 part_index: 99,
5589 staff_index: 0,
5590 clef: Clef::Bass,
5591 }),
5592 &mut score
5593 )
5594 .is_err()
5595 );
5596 }
5597
5598 #[test]
5599 fn set_part_name_updates_name() {
5600 let mut score = default_engine_score();
5601 apply_command(
5602 &Command::SetPartName(SetPartNameCmd {
5603 part_index: 0,
5604 name: "Violin".into(),
5605 short_name: "Vln.".into(),
5606 }),
5607 &mut score,
5608 )
5609 .unwrap();
5610 assert_eq!(score.parts[0].name, "Violin");
5611 assert_eq!(score.parts[0].short_name, "Vln.");
5612 }
5613
5614 #[test]
5615 fn set_part_name_undo_restores_old() {
5616 let mut stack = CommandStack::new(50);
5617 let mut score = default_engine_score();
5618 let original = score.parts[0].name.clone();
5619 stack
5620 .execute(
5621 Command::SetPartName(SetPartNameCmd {
5622 part_index: 0,
5623 name: "Flute".into(),
5624 short_name: "Fl.".into(),
5625 }),
5626 &mut score,
5627 )
5628 .unwrap();
5629 stack.undo(&mut score).unwrap();
5630 assert_eq!(score.parts[0].name, original);
5631 }
5632
5633 #[test]
5634 fn set_metadata_undo_restores_old_title() {
5635 let mut stack = CommandStack::new(50);
5636 let mut score = default_engine_score();
5637 let original = score.metadata.title.clone();
5638 stack
5639 .execute(
5640 Command::SetMetadata(SetMetadataCmd {
5641 title: Some("Changed".into()),
5642 ..Default::default()
5643 }),
5644 &mut score,
5645 )
5646 .unwrap();
5647 assert_ne!(score.metadata.title, original);
5648 stack.undo(&mut score).unwrap();
5649 assert_eq!(score.metadata.title, original);
5650 }
5651
5652 #[test]
5653 fn set_midi_instrument_updates_channel_and_program() {
5654 let mut score = default_engine_score();
5655 apply_command(
5656 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
5657 part_index: 0,
5658 midi_channel: 2,
5659 midi_program: 40,
5660 }),
5661 &mut score,
5662 )
5663 .unwrap();
5664 assert_eq!(score.parts[0].midi_channel, 2);
5665 assert_eq!(score.parts[0].midi_program, 40);
5666 }
5667
5668 #[test]
5669 fn set_midi_instrument_clamps_channel_to_15() {
5670 let mut score = default_engine_score();
5671 apply_command(
5672 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
5673 part_index: 0,
5674 midi_channel: 20,
5675 midi_program: 0,
5676 }),
5677 &mut score,
5678 )
5679 .unwrap();
5680 assert_eq!(score.parts[0].midi_channel, 15);
5681 }
5682
5683 #[test]
5684 fn set_midi_instrument_undo_restores_old() {
5685 let mut stack = CommandStack::new(50);
5686 let mut score = default_engine_score();
5687 score.parts[0].midi_channel = 3;
5688 score.parts[0].midi_program = 10;
5689 stack
5690 .execute(
5691 Command::SetMidiInstrument(SetMidiInstrumentCmd {
5692 part_index: 0,
5693 midi_channel: 9,
5694 midi_program: 114,
5695 }),
5696 &mut score,
5697 )
5698 .unwrap();
5699 stack.undo(&mut score).unwrap();
5700 assert_eq!(score.parts[0].midi_channel, 3);
5701 assert_eq!(score.parts[0].midi_program, 10);
5702 }
5703
5704 #[test]
5705 fn set_transpose_updates_staff() {
5706 let mut score = default_engine_score();
5707 apply_command(
5708 &Command::SetTranspose(SetTransposeCmd {
5709 part_index: 0,
5710 staff_index: 0,
5711 semitones: -2,
5712 }),
5713 &mut score,
5714 )
5715 .unwrap();
5716 assert_eq!(score.parts[0].staves[0].transpose_semitones, -2);
5717 }
5718
5719 #[test]
5720 fn set_transpose_out_of_range_returns_err() {
5721 let mut score = default_engine_score();
5722 assert!(
5723 apply_command(
5724 &Command::SetTranspose(SetTransposeCmd {
5725 part_index: 99,
5726 staff_index: 0,
5727 semitones: -2,
5728 }),
5729 &mut score
5730 )
5731 .is_err()
5732 );
5733 }
5734
5735 #[test]
5736 fn transpose_staff_region_is_undoable_and_json_compatible() {
5737 let mut stack = CommandStack::new(50);
5738 let mut score = Score::new("Region", 120, 4, 4, 0, 2);
5739 for measure in &mut score.parts[0].staves[0].measures {
5740 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5741 }
5742 let command = Command::TransposeStaffRegion(TransposeStaffRegionCmd {
5743 part_index: 0,
5744 staff_index: 0,
5745 start_measure: 1,
5746 end_measure: 2,
5747 semitones: 2,
5748 target: RegionalTranspositionTarget::Written,
5749 });
5750 let json = serde_json::to_string(&command).expect("command serializes");
5751 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
5752 assert_eq!(command_key(&decoded), "TransposeStaffRegion");
5753 stack.execute(decoded, &mut score).expect("region applies");
5754 assert_eq!(
5755 score.parts[0].staves[0].measures[0].voices[0][0].pitches[0].to_midi(),
5756 60
5757 );
5758 assert_eq!(
5759 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0].to_midi(),
5760 62
5761 );
5762 stack.undo(&mut score).expect("region undo applies");
5763 assert_eq!(
5764 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0].to_midi(),
5765 60
5766 );
5767 }
5768
5769 #[test]
5770 fn set_tempo_at_measure_sets_tempo() {
5771 let mut score = default_engine_score();
5772 apply_command(
5773 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
5774 measure_index: 0,
5775 bpm: Some(80),
5776 }),
5777 &mut score,
5778 )
5779 .unwrap();
5780 assert_eq!(score.parts[0].staves[0].measures[0].tempo, Some(80));
5781 }
5782
5783 #[test]
5784 fn set_tempo_at_measure_none_clears_tempo() {
5785 let mut score = default_engine_score();
5786 score.parts[0].staves[0].measures[0].tempo = Some(120);
5787 apply_command(
5788 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
5789 measure_index: 0,
5790 bpm: None,
5791 }),
5792 &mut score,
5793 )
5794 .unwrap();
5795 assert!(score.parts[0].staves[0].measures[0].tempo.is_none());
5796 }
5797
5798 #[test]
5799 fn set_tempo_ramp_at_measure_applies() {
5800 let mut score = default_engine_score();
5801 apply_command(
5802 &Command::SetTempoRampAtMeasure(SetTempoRampAtMeasureCmd {
5803 measure_index: 0,
5804 target_bpm: Some(72),
5805 }),
5806 &mut score,
5807 )
5808 .expect("tempo ramp applies");
5809 assert_eq!(score.parts[0].staves[0].measures[0].tempo_ramp_to, Some(72));
5810 }
5811
5812 #[test]
5815 fn batch_execute_two_commands_single_undo() {
5816 let mut stack = CommandStack::new(50);
5817 let mut score = default_engine_score();
5818 let original_bpm = score.settings.tempo_bpm;
5819 stack
5820 .batch_execute(
5821 vec![
5822 Command::SetTempo(SetTempoCmd { bpm: 160 }),
5823 Command::SetTempo(SetTempoCmd { bpm: 180 }),
5824 ],
5825 &mut score,
5826 )
5827 .unwrap();
5828 assert_eq!(score.settings.tempo_bpm, 180);
5829 stack.undo(&mut score).unwrap();
5830 assert_eq!(score.settings.tempo_bpm, original_bpm);
5831 }
5832
5833 #[test]
5834 fn batch_execute_partial_failure_rollback() {
5835 let mut stack = CommandStack::new(50);
5836 let mut score = default_engine_score();
5837 let original_bpm = score.settings.tempo_bpm;
5838 let result = stack.batch_execute(
5839 vec![
5840 Command::SetTempo(SetTempoCmd { bpm: 160 }),
5841 Command::DeleteNote(DeleteNoteCmd {
5842 note_id: "nonexistent".into(),
5843 part_index: 99,
5844 staff_index: 0,
5845 measure_index: 0,
5846 voice: 0,
5847 }),
5848 ],
5849 &mut score,
5850 );
5851 assert!(result.is_err());
5852 assert_eq!(score.settings.tempo_bpm, original_bpm);
5853 }
5854
5855 #[test]
5856 fn execute_rejects_invalid_candidate_without_mutating_or_recording_history() {
5857 let mut stack = CommandStack::new(50);
5858 let mut score = default_engine_score();
5859 score.parts[0].staves[0].tablature = Some(crate::TablatureConfig {
5860 lines: 6,
5861 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5862 capo: 0,
5863 });
5864 stack
5865 .execute(
5866 Command::AddNote(AddNoteCmd {
5867 part_index: 0,
5868 staff_index: 0,
5869 measure_index: 0,
5870 voice: 0,
5871 position: 0,
5872 pitch: Some(Pitch::new(Step::C, 4)),
5873 duration: Duration::Quarter,
5874 dot_count: 0,
5875 is_rest: false,
5876 tuplet: None,
5877 }),
5878 &mut score,
5879 )
5880 .expect("valid pitched note should be accepted");
5881 let original = score.clone();
5882 let result = stack.execute(
5883 Command::SetTabPosition(SetTabPositionCmd {
5884 part_index: 0,
5885 staff_index: 0,
5886 measure_index: 0,
5887 voice: 0,
5888 note_index: 0,
5889 position: Some(crate::TabPosition { string: 7, fret: 0 }),
5890 }),
5891 &mut score,
5892 );
5893 assert!(matches!(result, Err(Error::InvalidScore)));
5894 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
5895 let original_note = &original.parts[0].staves[0].measures[0].voices[0][0];
5896 assert_eq!(note.pitches, original_note.pitches);
5897 assert_eq!(note.duration, original_note.duration);
5898 assert_eq!(note.tab_position, original_note.tab_position);
5899 assert_eq!(note.tab_positions, original_note.tab_positions);
5900 assert!(stack.can_undo());
5901 }
5902
5903 #[test]
5904 fn set_tablature_config_is_undoable_and_json_compatible() {
5905 let mut stack = CommandStack::new(50);
5906 let mut score = default_engine_score();
5907 let config = crate::TablatureConfig {
5908 lines: 6,
5909 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5910 capo: 2,
5911 };
5912 let command = Command::SetTablatureConfig(SetTablatureConfigCmd {
5913 part_index: 0,
5914 staff_index: 0,
5915 config: Some(config.clone()),
5916 });
5917 let json = serde_json::to_string(&command).expect("command should serialize");
5918 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
5919 assert_eq!(command_key(&decoded), "SetTablatureConfig");
5920 stack
5921 .execute(decoded, &mut score)
5922 .expect("config should apply");
5923 assert_eq!(score.parts[0].staves[0].tablature, Some(config));
5924 assert_eq!(
5925 score.parts[0].staves[0].presentation.kind,
5926 StaffKind::Tablature
5927 );
5928 stack.undo(&mut score).expect("config undo should apply");
5929 assert!(score.parts[0].staves[0].tablature.is_none());
5930 assert_eq!(
5931 score.parts[0].staves[0].presentation.kind,
5932 StaffKind::Standard
5933 );
5934 stack.redo(&mut score).expect("config redo should apply");
5935 assert_eq!(
5936 score.parts[0].staves[0]
5937 .tablature
5938 .as_ref()
5939 .map(|tab| tab.capo),
5940 Some(2)
5941 );
5942 }
5943
5944 #[test]
5945 fn set_staff_presentation_is_undoable_and_requires_tablature_config() {
5946 let mut stack = CommandStack::new(50);
5947 let mut score = default_engine_score();
5948 let tab = StaffPresentation {
5949 kind: StaffKind::Tablature,
5950 lines: 6,
5951 line_distance: 1.25,
5952 small: true,
5953 cutaway: false,
5954 visible: true,
5955 notehead_scheme: super::super::score::StaffNoteheadScheme::Standard,
5956 tablature_rhythm_display: super::super::score::TablatureRhythmDisplay::FretOnly,
5957 tablature_fret_mark_style: super::super::score::TablatureFretMarkStyle::Arabic,
5958 };
5959 let rejected = Command::SetStaffPresentation(SetStaffPresentationCmd {
5960 part_index: 0,
5961 staff_index: 0,
5962 presentation: tab.clone(),
5963 });
5964 let before = score.clone();
5965 assert!(stack.execute(rejected, &mut score).is_err());
5966 assert_eq!(
5967 score.parts[0].staves[0].presentation,
5968 before.parts[0].staves[0].presentation
5969 );
5970 assert!(!stack.can_undo());
5971
5972 stack
5973 .execute(
5974 Command::SetTablatureConfig(SetTablatureConfigCmd {
5975 part_index: 0,
5976 staff_index: 0,
5977 config: Some(crate::TablatureConfig {
5978 lines: 6,
5979 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5980 capo: 0,
5981 }),
5982 }),
5983 &mut score,
5984 )
5985 .expect("tablature configuration should apply");
5986 stack
5987 .execute(
5988 Command::SetStaffPresentation(SetStaffPresentationCmd {
5989 part_index: 0,
5990 staff_index: 0,
5991 presentation: tab.clone(),
5992 }),
5993 &mut score,
5994 )
5995 .expect("staff presentation should apply");
5996 assert_eq!(stack.undo_key().as_deref(), Some("SetStaffPresentation"));
5997 assert_eq!(score.parts[0].staves[0].presentation, tab);
5998 stack
5999 .undo(&mut score)
6000 .expect("presentation undo should apply");
6001 assert_eq!(
6002 score.parts[0].staves[0].presentation.kind,
6003 StaffKind::Tablature
6004 );
6005 stack
6006 .redo(&mut score)
6007 .expect("presentation redo should apply");
6008 assert_eq!(
6009 score.parts[0].staves[0].presentation.kind,
6010 StaffKind::Tablature
6011 );
6012 }
6013
6014 #[test]
6015 fn set_instrument_definition_is_undoable_and_rejects_invalid_ranges() {
6016 let mut stack = CommandStack::new(50);
6017 let mut score = default_engine_score();
6018 let mut invalid = InstrumentDefinition::new("violin", "Violin");
6019 invalid.written_range = Some(InstrumentRange {
6020 lowest: 100,
6021 highest: 55,
6022 });
6023 let before = score.clone();
6024 assert!(
6025 stack
6026 .execute(
6027 Command::SetInstrumentDefinition(SetInstrumentDefinitionCmd {
6028 part_index: 0,
6029 definition: Some(invalid),
6030 }),
6031 &mut score,
6032 )
6033 .is_err()
6034 );
6035 assert_eq!(score.parts[0].instrument, before.parts[0].instrument);
6036
6037 let mut definition = InstrumentDefinition::new("violin", "Violin");
6038 definition.short_name = "Vln.".to_string();
6039 definition.family = Some("strings".to_string());
6040 definition.written_range = Some(InstrumentRange {
6041 lowest: 55,
6042 highest: 103,
6043 });
6044 definition.sounding_range = definition.written_range;
6045 definition.default_clefs = vec![Clef::Treble];
6046 definition.midi_program = 40;
6047 let command = Command::SetInstrumentDefinition(SetInstrumentDefinitionCmd {
6048 part_index: 0,
6049 definition: Some(definition.clone()),
6050 });
6051 let json = serde_json::to_string(&command).expect("command should serialize");
6052 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6053 assert_eq!(command_key(&decoded), "SetInstrumentDefinition");
6054 stack
6055 .execute(decoded, &mut score)
6056 .expect("definition applies");
6057 assert_eq!(score.parts[0].instrument, Some(definition));
6058 stack.undo(&mut score).expect("definition undo applies");
6059 assert!(score.parts[0].instrument.is_none());
6060 stack.redo(&mut score).expect("definition redo applies");
6061 assert_eq!(
6062 score.parts[0]
6063 .instrument
6064 .as_ref()
6065 .map(|value| value.id.as_str()),
6066 Some("violin")
6067 );
6068 }
6069
6070 #[test]
6071 fn set_percussion_kit_is_undoable_and_rejects_invalid_entries() {
6072 let mut stack = CommandStack::new(50);
6073 let mut score = default_engine_score();
6074 let invalid = Command::SetPercussionKit(SetPercussionKitCmd {
6075 part_index: 0,
6076 instruments: vec![PercussionInstrument {
6077 id: "snare".to_string(),
6078 name: None,
6079 midi_unpitched: Some(38),
6080 staff_position: Some(33),
6081 notehead: None,
6082 preferred_voice: None,
6083 techniques: Vec::new(),
6084 }],
6085 });
6086 assert!(stack.execute(invalid, &mut score).is_err());
6087 assert!(score.parts[0].percussion_instruments.is_empty());
6088
6089 let kit = vec![PercussionInstrument {
6090 id: "snare".to_string(),
6091 name: Some("Acoustic Snare".to_string()),
6092 midi_unpitched: Some(38),
6093 staff_position: Some(0),
6094 notehead: Some(NoteHead::Cross),
6095 preferred_voice: Some(1),
6096 techniques: vec!["rim-shot".to_string()],
6097 }];
6098 let command = Command::SetPercussionKit(SetPercussionKitCmd {
6099 part_index: 0,
6100 instruments: kit.clone(),
6101 });
6102 let json = serde_json::to_string(&command).expect("command serializes");
6103 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6104 assert_eq!(command_key(&decoded), "SetPercussionKit");
6105 stack.execute(decoded, &mut score).expect("kit applies");
6106 assert_eq!(score.parts[0].percussion_instruments, kit);
6107 stack.undo(&mut score).expect("kit undo applies");
6108 assert!(score.parts[0].percussion_instruments.is_empty());
6109 stack.redo(&mut score).expect("kit redo applies");
6110 assert_eq!(score.parts[0].percussion_instruments, kit);
6111 }
6112
6113 #[test]
6114 fn set_measure_instrument_change_is_undoable_and_json_compatible() {
6115 let mut stack = CommandStack::new(50);
6116 let mut score = default_engine_score();
6117 let mut definition = InstrumentDefinition::new("clarinet-bb", "B-flat Clarinet");
6118 definition.transpose_semitones = -2;
6119 definition.midi_program = 71;
6120 let command = Command::SetMeasureInstrumentChange(SetMeasureInstrumentChangeCmd {
6121 part_index: 0,
6122 staff_index: 0,
6123 measure_index: 2,
6124 definition: Some(definition.clone()),
6125 });
6126 let json = serde_json::to_string(&command).expect("command should serialize");
6127 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6128 assert_eq!(command_key(&decoded), "SetMeasureInstrumentChange");
6129 stack.execute(decoded, &mut score).expect("change applies");
6130 assert_eq!(
6131 score.parts[0].staves[0].measures[2].instrument_change,
6132 Some(definition)
6133 );
6134 stack.undo(&mut score).expect("undo applies");
6135 assert!(
6136 score.parts[0].staves[0].measures[2]
6137 .instrument_change
6138 .is_none()
6139 );
6140 }
6141
6142 #[test]
6143 fn set_measure_tablature_change_is_undoable_and_uses_base_line_count() {
6144 let mut stack = CommandStack::new(50);
6145 let mut score = default_engine_score();
6146 let base = crate::TablatureConfig {
6147 lines: 6,
6148 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6149 capo: 0,
6150 };
6151 score.parts[0].staves[0].tablature = Some(base.clone());
6152 let mut changed = base;
6153 changed.tuning_midi[0] = 38;
6154 changed.capo = 2;
6155 let command = Command::SetMeasureTablatureChange(SetMeasureTablatureChangeCmd {
6156 part_index: 0,
6157 staff_index: 0,
6158 measure_index: 2,
6159 config: Some(changed.clone()),
6160 });
6161 let json = serde_json::to_string(&command).expect("command serializes");
6162 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6163 assert_eq!(command_key(&decoded), "SetMeasureTablatureChange");
6164 stack.execute(decoded, &mut score).expect("change applies");
6165 assert_eq!(
6166 score.parts[0].staves[0].tablature_at(1),
6167 score.parts[0].staves[0].tablature
6168 );
6169 assert_eq!(score.parts[0].staves[0].tablature_at(2), Some(changed));
6170 stack.undo(&mut score).expect("change undo applies");
6171 assert!(
6172 score.parts[0].staves[0].measures[2]
6173 .tablature_change
6174 .is_none()
6175 );
6176
6177 let invalid = Command::SetMeasureTablatureChange(SetMeasureTablatureChangeCmd {
6178 part_index: 0,
6179 staff_index: 0,
6180 measure_index: 1,
6181 config: Some(crate::TablatureConfig {
6182 lines: 7,
6183 tuning_midi: vec![40, 45, 50, 55, 59, 64, 69],
6184 capo: 0,
6185 }),
6186 });
6187 assert!(stack.execute(invalid, &mut score).is_err());
6188 }
6189
6190 #[test]
6191 fn batch_execute_empty_is_noop() {
6192 let mut stack = CommandStack::new(50);
6193 let mut score = default_engine_score();
6194 stack.batch_execute(vec![], &mut score).unwrap();
6195 assert!(!stack.can_undo());
6196 }
6197
6198 #[test]
6201 fn batch_label_used_as_command_key() {
6202 let cmd = Command::Batch(BatchCmd {
6203 commands: vec![],
6204 label: Some("ApplyAI".to_string()),
6205 });
6206 assert_eq!(command_key(&cmd), "ApplyAI");
6207 }
6208
6209 #[test]
6210 fn batch_no_label_key_is_batch() {
6211 let cmd = Command::Batch(BatchCmd {
6212 commands: vec![],
6213 label: None,
6214 });
6215 assert_eq!(command_key(&cmd), "Batch");
6216 }
6217
6218 #[test]
6219 fn batch_label_survives_json_roundtrip() {
6220 let cmd = Command::Batch(BatchCmd {
6221 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 120 })],
6222 label: Some("PasteSelection".to_string()),
6223 });
6224 let json = serde_json::to_string(&cmd).unwrap();
6225 let cmd2: Command = serde_json::from_str(&json).unwrap();
6226 assert_eq!(command_key(&cmd2), "PasteSelection");
6227 }
6228
6229 #[test]
6230 fn batch_label_in_undo_key() {
6231 let mut stack = CommandStack::new(50);
6232 let mut score = default_engine_score();
6233 let cmd = Command::Batch(BatchCmd {
6234 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 140 })],
6235 label: Some("ApplyAI".to_string()),
6236 });
6237 stack.execute(cmd, &mut score).unwrap();
6238 assert_eq!(stack.undo_key(), Some("ApplyAI".to_string()));
6239 }
6240
6241 #[test]
6242 fn undo_returns_change_hint() {
6243 use crate::model::change_hint::ChangeScope;
6244 let mut stack = CommandStack::new(50);
6245 let mut score = default_engine_score();
6246 stack
6247 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
6248 .unwrap();
6249 let hint = stack.undo(&mut score).unwrap();
6250 assert_eq!(hint.scope, ChangeScope::Global);
6251 assert!(hint.playback_dirty);
6252 }
6253
6254 #[test]
6255 fn redo_returns_change_hint() {
6256 use crate::model::change_hint::ChangeScope;
6257 let mut stack = CommandStack::new(50);
6258 let mut score = default_engine_score();
6259 stack
6260 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
6261 .unwrap();
6262 stack.undo(&mut score).unwrap();
6263 let hint = stack.redo(&mut score).unwrap();
6264 assert_eq!(hint.scope, ChangeScope::Global);
6265 assert!(hint.playback_dirty);
6266 }
6267
6268 #[test]
6271 fn toggle_slur_sets_start_and_end() {
6272 let mut score = default_engine_score();
6273 let cmd = Command::AddNote(AddNoteCmd {
6274 part_index: 0,
6275 staff_index: 0,
6276 measure_index: 0,
6277 voice: 0,
6278 position: 0,
6279 pitch: Some(Pitch::new(Step::C, 4)),
6280 duration: Duration::Quarter,
6281 dot_count: 0,
6282 is_rest: false,
6283 tuplet: None,
6284 });
6285 apply_command(&cmd, &mut score).unwrap();
6286 apply_command(
6287 &Command::AddNote(AddNoteCmd {
6288 part_index: 0,
6289 staff_index: 0,
6290 measure_index: 0,
6291 voice: 0,
6292 position: 1,
6293 pitch: Some(Pitch::new(Step::D, 4)),
6294 duration: Duration::Quarter,
6295 dot_count: 0,
6296 is_rest: false,
6297 tuplet: None,
6298 }),
6299 &mut score,
6300 )
6301 .unwrap();
6302 let start = NoteAddr {
6303 part: 0,
6304 staff: 0,
6305 measure: 0,
6306 voice: 0,
6307 note: 0,
6308 };
6309 let end = NoteAddr {
6310 part: 0,
6311 staff: 0,
6312 measure: 0,
6313 voice: 0,
6314 note: 1,
6315 };
6316 apply_command(
6317 &Command::ToggleSlur(ToggleSlurCmd {
6318 start: start.clone(),
6319 end: end.clone(),
6320 }),
6321 &mut score,
6322 )
6323 .unwrap();
6324 assert!(score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
6325 assert!(score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
6326 apply_command(
6328 &Command::ToggleSlur(ToggleSlurCmd { start, end }),
6329 &mut score,
6330 )
6331 .unwrap();
6332 assert!(!score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
6333 assert!(!score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
6334 }
6335
6336 #[test]
6339 fn add_staff_appends_staff_with_correct_measure_count() {
6340 let mut score = default_engine_score();
6341 let before = score.parts[0].staves.len();
6342 let measure_count = score.parts[0].staves[0].measures.len();
6343 apply_command(
6344 &Command::AddStaff(AddStaffCmd {
6345 part_index: 0,
6346 clef: Clef::Bass,
6347 }),
6348 &mut score,
6349 )
6350 .unwrap();
6351 assert_eq!(score.parts[0].staves.len(), before + 1);
6352 let new_staff = score.parts[0].staves.last().unwrap();
6353 assert_eq!(new_staff.measures.len(), measure_count);
6354 }
6355
6356 #[test]
6357 fn add_staff_out_of_range_returns_err() {
6358 let mut score = default_engine_score();
6359 let result = apply_command(
6360 &Command::AddStaff(AddStaffCmd {
6361 part_index: 99,
6362 clef: Clef::Treble,
6363 }),
6364 &mut score,
6365 );
6366 assert!(result.is_err());
6367 }
6368
6369 #[test]
6370 fn delete_staff_removes_extra_staff() {
6371 let mut score = default_engine_score();
6372 apply_command(
6373 &Command::AddStaff(AddStaffCmd {
6374 part_index: 0,
6375 clef: Clef::Bass,
6376 }),
6377 &mut score,
6378 )
6379 .unwrap();
6380 assert_eq!(score.parts[0].staves.len(), 2);
6381 apply_command(
6382 &Command::DeleteStaff(DeleteStaffCmd {
6383 part_index: 0,
6384 staff_index: 1,
6385 }),
6386 &mut score,
6387 )
6388 .unwrap();
6389 assert_eq!(score.parts[0].staves.len(), 1);
6390 }
6391
6392 #[test]
6393 fn delete_last_staff_returns_err() {
6394 let mut score = default_engine_score();
6395 assert_eq!(score.parts[0].staves.len(), 1);
6396 let result = apply_command(
6397 &Command::DeleteStaff(DeleteStaffCmd {
6398 part_index: 0,
6399 staff_index: 0,
6400 }),
6401 &mut score,
6402 );
6403 assert!(result.is_err());
6404 }
6405
6406 #[test]
6409 fn set_tuplet_assigns_and_clears() {
6410 use crate::model::notation::TupletInfo;
6411 let mut score = default_engine_score();
6412 apply_command(
6413 &Command::AddNote(AddNoteCmd {
6414 part_index: 0,
6415 staff_index: 0,
6416 measure_index: 0,
6417 voice: 0,
6418 position: 0,
6419 pitch: Some(Pitch::new(Step::C, 4)),
6420 duration: Duration::Quarter,
6421 dot_count: 0,
6422 is_rest: false,
6423 tuplet: None,
6424 }),
6425 &mut score,
6426 )
6427 .unwrap();
6428 let ti = TupletInfo {
6429 actual_notes: 3,
6430 normal_notes: 2,
6431 };
6432 apply_command(
6433 &Command::SetTuplet(SetTupletCmd {
6434 part_index: 0,
6435 staff_index: 0,
6436 measure_index: 0,
6437 voice_index: 0,
6438 note_index: 0,
6439 tuplet: Some(ti.clone()),
6440 }),
6441 &mut score,
6442 )
6443 .unwrap();
6444 assert_eq!(
6445 score.parts[0].staves[0].measures[0].voices[0][0].tuplet,
6446 Some(ti)
6447 );
6448 apply_command(
6449 &Command::SetTuplet(SetTupletCmd {
6450 part_index: 0,
6451 staff_index: 0,
6452 measure_index: 0,
6453 voice_index: 0,
6454 note_index: 0,
6455 tuplet: None,
6456 }),
6457 &mut score,
6458 )
6459 .unwrap();
6460 assert!(
6461 score.parts[0].staves[0].measures[0].voices[0][0]
6462 .tuplet
6463 .is_none()
6464 );
6465 }
6466
6467 #[test]
6470 fn respell_score_cmd_changes_all_pitches() {
6471 use crate::model::pitch::Step;
6472 let mut score = default_engine_score();
6473 apply_command(
6474 &Command::AddNote(AddNoteCmd {
6475 part_index: 0,
6476 staff_index: 0,
6477 measure_index: 0,
6478 voice: 0,
6479 position: 0,
6480 pitch: Some(Pitch::with_alter(Step::C, 4, 1)), duration: Duration::Quarter,
6482 dot_count: 0,
6483 is_rest: false,
6484 tuplet: None,
6485 }),
6486 &mut score,
6487 )
6488 .unwrap();
6489 apply_command(
6490 &Command::RespellScore(RespellScoreCmd { prefer_flat: true }),
6491 &mut score,
6492 )
6493 .unwrap();
6494 let pitch = &score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
6495 assert_eq!(pitch.step, Step::D);
6496 assert_eq!(pitch.alter, -1); }
6498
6499 #[test]
6500 fn typed_spanner_commands_are_atomic_and_undoable() {
6501 use crate::model::score::{NotationSpanner, NotationSpannerKind};
6502
6503 let mut score = default_engine_score();
6504 let address = NoteAddr {
6505 part: 0,
6506 staff: 0,
6507 measure: 0,
6508 voice: 0,
6509 note: 0,
6510 };
6511 let spanner = NotationSpanner {
6512 id: "slur-1".to_string(),
6513 kind: NotationSpannerKind::Slur,
6514 start: address.clone(),
6515 end: address,
6516 number: Some(1),
6517 line_type: Some("dashed".to_string()),
6518 text: None,
6519 placement: Some("above".to_string()),
6520 ottava_size: None,
6521 ottava_type: None,
6522 };
6523 let mut stack = CommandStack::new(8);
6524 stack
6525 .execute(Command::AddSpanner(AddSpannerCmd { spanner }), &mut score)
6526 .unwrap();
6527 assert_eq!(score.spanners.len(), 1);
6528 assert_eq!(stack.undo_key().as_deref(), Some("AddSpanner"));
6529
6530 let before_duplicate = score.clone();
6531 let duplicate = score.spanners[0].clone();
6532 assert!(
6533 stack
6534 .execute(
6535 Command::AddSpanner(AddSpannerCmd { spanner: duplicate }),
6536 &mut score,
6537 )
6538 .is_err()
6539 );
6540 assert_eq!(score.spanners, before_duplicate.spanners);
6541
6542 let mut updated = score.spanners[0].clone();
6543 updated.number = Some(2);
6544 stack
6545 .execute(
6546 Command::UpdateSpanner(UpdateSpannerCmd { spanner: updated }),
6547 &mut score,
6548 )
6549 .unwrap();
6550 assert_eq!(score.spanners[0].number, Some(2));
6551 stack.undo(&mut score).unwrap();
6552 assert_eq!(score.spanners[0].number, Some(1));
6553 stack.redo(&mut score).unwrap();
6554 assert_eq!(score.spanners[0].number, Some(2));
6555
6556 stack
6557 .execute(
6558 Command::RemoveSpanner(RemoveSpannerCmd {
6559 id: "slur-1".to_string(),
6560 }),
6561 &mut score,
6562 )
6563 .unwrap();
6564 assert!(score.spanners.is_empty());
6565 stack.undo(&mut score).unwrap();
6566 assert_eq!(score.spanners.len(), 1);
6567 }
6568
6569 #[test]
6570 fn string_quartet_linked_views_roundtrip_and_undo_redo_as_one_contract() {
6571 let mut score = Score::template(ScoreTemplate::StringQuartet);
6572 let mut stack = CommandStack::new(16);
6573 for (part, id, name) in [
6574 (0, "violin-1", "Violin I"),
6575 (1, "violin-2", "Violin II"),
6576 (2, "viola", "Viola"),
6577 (3, "cello", "Cello"),
6578 ] {
6579 stack
6580 .execute(
6581 Command::UpsertScoreView(UpsertScoreViewCmd {
6582 view: ScoreView::linked_part(id, name, part),
6583 }),
6584 &mut score,
6585 )
6586 .expect("linked part view applies");
6587 }
6588 assert_eq!(score.parts.len(), 4);
6589 assert_eq!(score.views.len(), 4);
6590 for (part, view) in score.views.iter().enumerate() {
6591 assert_eq!(view.parts, vec![part]);
6592 assert_eq!(score.resolve_view(&view.id).unwrap().parts.len(), 1);
6593 }
6594
6595 let restored: Score =
6596 serde_json::from_str(&serde_json::to_string(&score).expect("quartet score serializes"))
6597 .expect("quartet score deserializes");
6598 assert_eq!(restored.views, score.views);
6599
6600 for _ in 0..4 {
6601 stack.undo(&mut score).expect("view undo applies");
6602 }
6603 assert!(score.views.is_empty());
6604 for _ in 0..4 {
6605 stack.redo(&mut score).expect("view redo applies");
6606 }
6607 assert_eq!(score.views, restored.views);
6608 }
6609
6610 #[test]
6611 fn section_break_is_atomic_undoable_and_synced_across_staves() {
6612 let mut score = Score::template(ScoreTemplate::Piano);
6613 let original = score.clone();
6614 let mut stack = CommandStack::new(4);
6615 stack
6616 .execute(
6617 Command::SetSectionBreak(SetSectionBreakCmd {
6618 measure_index: 2,
6619 value: true,
6620 }),
6621 &mut score,
6622 )
6623 .expect("section break applies");
6624 assert!(
6625 score
6626 .parts
6627 .iter()
6628 .flat_map(|part| part.staves.iter())
6629 .all(|staff| staff.measures[2].section_break)
6630 );
6631 stack.undo(&mut score).expect("section break undoes");
6632 assert_eq!(
6633 serde_json::to_value(&score).unwrap(),
6634 serde_json::to_value(&original).unwrap()
6635 );
6636
6637 assert!(
6638 stack
6639 .execute(
6640 Command::SetSectionBreak(SetSectionBreakCmd {
6641 measure_index: 99,
6642 value: true,
6643 }),
6644 &mut score,
6645 )
6646 .is_err()
6647 );
6648 assert_eq!(
6649 serde_json::to_value(&score).unwrap(),
6650 serde_json::to_value(&original).unwrap()
6651 );
6652 }
6653}