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