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