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