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 let mut total = 0.0f64;
4893 let mut cutoff = voice.len();
4894 for (i, n) in voice.iter().enumerate() {
4895 total += n.beats();
4896 if total > max_beats + 1e-9 {
4897 cutoff = i;
4898 break;
4899 }
4900 }
4901 voice.truncate(cutoff);
4902 pad_voice_to_measure(voice, max_beats);
4903}
4904
4905fn pad_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
4906 let mut used: f64 = voice.iter().map(|n| n.beats()).sum();
4907 while max_beats - used > 1e-9 {
4908 let remaining = max_beats - used;
4909 let rest = Note::rest(Duration::whole_filling_beats(remaining));
4910 used += rest.beats();
4911 voice.push(rest);
4912 }
4913}
4914
4915#[cfg(test)]
4916mod tests {
4917 use super::*;
4918 use crate::ScoreEngine;
4919 use crate::model::pitch::Step;
4920
4921 fn default_engine_score() -> Score {
4922 let mut s = Score::default();
4923 for part in &mut s.parts {
4924 for staff in &mut part.staves {
4925 for (i, m) in staff.measures.iter_mut().enumerate() {
4926 m.number = i as u32 + 1;
4927 }
4928 }
4929 }
4930 s
4931 }
4932
4933 #[test]
4934 fn legacy_scale_voice_range_command_defaults_to_preserve_tuplet_ratio() {
4935 let command: Command = serde_json::from_str(
4936 r#"{"type":"scale_voice_range","part_index":0,"staff_index":0,"voice":0,"start_measure":0,"end_measure":0,"scale":"half"}"#,
4937 )
4938 .expect("legacy scale command deserializes");
4939 let Command::ScaleVoiceRange(command) = command else {
4940 panic!("expected scale command");
4941 };
4942 assert_eq!(command.tuplet_policy, TupletScalePolicy::PreserveRatio);
4943 }
4944
4945 fn score_with_typed_spanner() -> Score {
4946 use crate::model::score::{NotationSpanner, NotationSpannerKind};
4947
4948 let mut score = Score::new("Spanner edits", 120, 4, 4, 0, 1);
4949 score.parts[0].staves[0].measures[0].voices[0] = (0..4)
4950 .map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
4951 .collect();
4952 score.spanners.push(NotationSpanner {
4953 id: "typed-glissando".to_string(),
4954 kind: NotationSpannerKind::Glissando,
4955 start: NoteAddr {
4956 part: 0,
4957 staff: 0,
4958 measure: 0,
4959 voice: 0,
4960 note: 1,
4961 },
4962 end: NoteAddr {
4963 part: 0,
4964 staff: 0,
4965 measure: 0,
4966 voice: 0,
4967 note: 2,
4968 },
4969 number: Some(1),
4970 line_type: None,
4971 text: None,
4972 placement: None,
4973 ottava_size: None,
4974 ottava_type: None,
4975 });
4976 score
4977 }
4978
4979 fn insert_note_at(position: usize) -> Command {
4980 Command::AddNote(AddNoteCmd {
4981 part_index: 0,
4982 staff_index: 0,
4983 measure_index: 0,
4984 voice: 0,
4985 position,
4986 pitch: Some(Pitch::new(Step::D, 5)),
4987 duration: Duration::Quarter,
4988 dot_count: 0,
4989 is_rest: false,
4990 tuplet: None,
4991 })
4992 }
4993
4994 #[test]
4995 fn add_note_inserts_into_voice() {
4996 let mut score = default_engine_score();
4997 let cmd = Command::AddNote(AddNoteCmd {
4998 part_index: 0,
4999 staff_index: 0,
5000 measure_index: 0,
5001 voice: 0,
5002 position: 0,
5003 pitch: Some(Pitch::new(Step::C, 4)),
5004 duration: Duration::Quarter,
5005 dot_count: 0,
5006 is_rest: false,
5007 tuplet: None,
5008 });
5009 apply_command(&cmd, &mut score).unwrap();
5010 let first = &score.parts[0].staves[0].measures[0].voices[0][0];
5011 assert!(!first.is_rest);
5012 assert_eq!(first.pitches[0].step, Step::C);
5013 }
5014
5015 #[test]
5016 fn typed_spanners_follow_note_insertions_at_every_relative_position() {
5017 for (position, expected_start, expected_end) in [
5018 (0, 2, 3), (1, 2, 3), (2, 1, 3), (4, 1, 2), (3, 1, 2), ] {
5024 let mut score = score_with_typed_spanner();
5025 apply_command(&insert_note_at(position), &mut score).unwrap();
5026 let span = score.spanners.first().expect("span remains attached");
5027 assert_eq!(span.start.note, expected_start, "position {position}");
5028 assert_eq!(span.end.note, expected_end, "position {position}");
5029 }
5030 }
5031
5032 #[test]
5033 fn deleting_a_typed_spanner_endpoint_removes_the_whole_span_and_undo_redo_is_atomic() {
5034 let mut score = score_with_typed_spanner();
5035 let endpoint_id = score.parts[0].staves[0].measures[0].voices[0][1].id.clone();
5036 let mut stack = CommandStack::new(8);
5037
5038 stack
5039 .execute(
5040 Command::DeleteNote(DeleteNoteCmd {
5041 note_id: endpoint_id,
5042 part_index: 0,
5043 staff_index: 0,
5044 measure_index: 0,
5045 voice: 0,
5046 }),
5047 &mut score,
5048 )
5049 .unwrap();
5050 assert!(score.spanners.is_empty());
5051
5052 stack.undo(&mut score).unwrap();
5053 assert_eq!(score.spanners[0].start.note, 1);
5054 assert_eq!(score.spanners[0].end.note, 2);
5055
5056 stack.redo(&mut score).unwrap();
5057 assert!(score.spanners.is_empty());
5058 }
5059
5060 #[test]
5061 fn batched_structural_edits_remap_typed_spanners_and_restore_them_on_undo() {
5062 let mut score = score_with_typed_spanner();
5063 let mut stack = CommandStack::new(8);
5064 stack
5065 .batch_execute(
5066 vec![
5067 insert_note_at(0),
5068 Command::SetTempo(SetTempoCmd { bpm: 144 }),
5069 ],
5070 &mut score,
5071 )
5072 .unwrap();
5073 assert_eq!(score.spanners[0].start.note, 2);
5074 assert_eq!(score.spanners[0].end.note, 3);
5075
5076 stack.undo(&mut score).unwrap();
5077 assert_eq!(score.spanners[0].start.note, 1);
5078 assert_eq!(score.spanners[0].end.note, 2);
5079
5080 stack.redo(&mut score).unwrap();
5081 assert_eq!(score.spanners[0].start.note, 2);
5082 assert_eq!(score.spanners[0].end.note, 3);
5083 }
5084
5085 #[test]
5086 fn typed_spanners_remap_only_the_edited_endpoint_across_staff_and_voice() {
5087 use crate::model::score::{NotationSpanner, NotationSpannerKind, ScoreTemplate};
5088
5089 let mut score = Score::template(ScoreTemplate::Piano);
5090 for staff in &mut score.parts[0].staves {
5091 let notes: Vec<Note> = (0..4)
5092 .map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
5093 .collect();
5094 staff.measures[0].voices[0] = notes.clone();
5095 staff.measures[0].voices[1] = notes;
5096 }
5097 score.spanners.push(NotationSpanner {
5098 id: "cross-staff".to_string(),
5099 kind: NotationSpannerKind::Slur,
5100 start: NoteAddr {
5101 part: 0,
5102 staff: 0,
5103 measure: 0,
5104 voice: 0,
5105 note: 1,
5106 },
5107 end: NoteAddr {
5108 part: 0,
5109 staff: 1,
5110 measure: 0,
5111 voice: 1,
5112 note: 2,
5113 },
5114 number: Some(2),
5115 line_type: None,
5116 text: None,
5117 placement: None,
5118 ottava_size: None,
5119 ottava_type: None,
5120 });
5121
5122 let mut stack = CommandStack::new(8);
5123 stack.execute(insert_note_at(0), &mut score).unwrap();
5124 let span = &score.spanners[0];
5125 assert_eq!(span.start.note, 2);
5126 assert_eq!(span.end.staff, 1);
5127 assert_eq!(span.end.voice, 1);
5128 assert_eq!(span.end.note, 2);
5129 }
5130
5131 #[test]
5132 fn set_fingerings_keeps_first_legacy_value_in_sync() {
5133 let mut score = default_engine_score();
5134 apply_command(
5135 &Command::AddNote(AddNoteCmd {
5136 part_index: 0,
5137 staff_index: 0,
5138 measure_index: 0,
5139 voice: 0,
5140 position: 0,
5141 pitch: Some(Pitch::new(Step::C, 4)),
5142 duration: Duration::Quarter,
5143 dot_count: 0,
5144 is_rest: false,
5145 tuplet: None,
5146 }),
5147 &mut score,
5148 )
5149 .unwrap();
5150 apply_command(
5151 &Command::SetFingerings(SetFingeringsCmd {
5152 part_index: 0,
5153 staff_index: 0,
5154 measure_index: 0,
5155 voice: 0,
5156 note_index: 0,
5157 fingerings: vec![1, 3, 4],
5158 }),
5159 &mut score,
5160 )
5161 .unwrap();
5162 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
5163 assert_eq!(note.fingerings, vec![1, 3, 4]);
5164 assert_eq!(note.fingering, Some(1));
5165 }
5166
5167 #[test]
5168 fn set_figured_bass_replaces_measure_figures() {
5169 let mut score = default_engine_score();
5170 let figures = vec![FiguredBassFigure {
5171 number: "6".to_string(),
5172 alter: Some("-1".to_string()),
5173 prefix: Some("+".to_string()),
5174 suffix: None,
5175 extender: false,
5176 }];
5177 apply_command(
5178 &Command::SetFiguredBass(SetFiguredBassCmd {
5179 measure_index: 0,
5180 figures: figures.clone(),
5181 }),
5182 &mut score,
5183 )
5184 .unwrap();
5185 assert_eq!(score.parts[0].staves[0].measures[0].figured_bass, figures);
5186 }
5187
5188 #[test]
5189 fn set_measure_text_supports_append_replace_remove_and_undo_redo() {
5190 let mut engine = crate::ScoreEngine::new();
5191 let address = SetMeasureTextCmd {
5192 part_index: 0,
5193 staff_index: 0,
5194 measure_index: 0,
5195 text_index: 0,
5196 text: Some(StyledText {
5197 style: crate::TextStyle::Technique,
5198 text: "dolce".to_string(),
5199 placement: None,
5200 offset_x: None,
5201 offset_y: None,
5202 relative_x: None,
5203 relative_y: None,
5204 }),
5205 };
5206 engine.apply(Command::SetMeasureText(address)).unwrap();
5207 assert_eq!(
5208 engine.score.parts[0].staves[0].measures[0].texts[0],
5209 StyledText {
5210 style: crate::TextStyle::Technique,
5211 text: "dolce".to_string(),
5212 placement: None,
5213 offset_x: None,
5214 offset_y: None,
5215 relative_x: None,
5216 relative_y: None,
5217 }
5218 );
5219
5220 engine
5221 .apply(Command::SetMeasureText(SetMeasureTextCmd {
5222 text_index: 0,
5223 text: Some(StyledText {
5224 style: crate::TextStyle::RehearsalMark,
5225 text: "A".to_string(),
5226 placement: None,
5227 offset_x: None,
5228 offset_y: None,
5229 relative_x: None,
5230 relative_y: None,
5231 }),
5232 ..SetMeasureTextCmd {
5233 part_index: 0,
5234 staff_index: 0,
5235 measure_index: 0,
5236 text_index: 0,
5237 text: None,
5238 }
5239 }))
5240 .unwrap();
5241 assert_eq!(
5242 engine.score.parts[0].staves[0].measures[0].texts[0].style,
5243 crate::TextStyle::RehearsalMark
5244 );
5245
5246 engine
5247 .apply(Command::SetMeasureText(SetMeasureTextCmd {
5248 text_index: 0,
5249 text: None,
5250 ..SetMeasureTextCmd {
5251 part_index: 0,
5252 staff_index: 0,
5253 measure_index: 0,
5254 text_index: 0,
5255 text: None,
5256 }
5257 }))
5258 .unwrap();
5259 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
5260 engine.undo().unwrap();
5261 assert_eq!(engine.score.parts[0].staves[0].measures[0].texts.len(), 1);
5262 engine.redo().unwrap();
5263 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
5264 }
5265
5266 #[test]
5267 fn set_measure_text_rejects_invalid_index_atomically_and_round_trips_json() {
5268 let mut score = default_engine_score();
5269 let before = score.clone();
5270 let command = Command::SetMeasureText(SetMeasureTextCmd {
5271 part_index: 0,
5272 staff_index: 0,
5273 measure_index: 0,
5274 text_index: 2,
5275 text: Some(StyledText {
5276 style: crate::TextStyle::Expression,
5277 text: "espressivo".to_string(),
5278 placement: None,
5279 offset_x: None,
5280 offset_y: None,
5281 relative_x: None,
5282 relative_y: None,
5283 }),
5284 });
5285 let json = serde_json::to_string(&command).unwrap();
5286 let restored: Command = serde_json::from_str(&json).unwrap();
5287 assert_eq!(
5288 serde_json::to_value(&restored).unwrap(),
5289 serde_json::to_value(&command).unwrap()
5290 );
5291 assert!(apply_command(&restored, &mut score).is_err());
5292 assert_eq!(
5293 serde_json::to_value(&score).unwrap(),
5294 serde_json::to_value(&before).unwrap()
5295 );
5296 }
5297
5298 #[test]
5299 fn set_score_text_supports_append_replace_remove_and_undo_redo() {
5300 let mut engine = crate::ScoreEngine::new();
5301 let text = StyledText {
5302 style: crate::TextStyle::Expression,
5303 text: "Title".to_string(),
5304 placement: None,
5305 offset_x: None,
5306 offset_y: None,
5307 relative_x: None,
5308 relative_y: None,
5309 };
5310 engine
5311 .apply(Command::SetScoreText(SetScoreTextCmd {
5312 text_index: 0,
5313 text: Some(text.clone()),
5314 }))
5315 .unwrap();
5316 assert_eq!(engine.score.texts, vec![text.clone()]);
5317
5318 let mut replacement = text.clone();
5319 replacement.text = "Subtitle".to_string();
5320 engine
5321 .apply(Command::SetScoreText(SetScoreTextCmd {
5322 text_index: 0,
5323 text: Some(replacement.clone()),
5324 }))
5325 .unwrap();
5326 assert_eq!(engine.score.texts, vec![replacement]);
5327
5328 engine
5329 .apply(Command::SetScoreText(SetScoreTextCmd {
5330 text_index: 0,
5331 text: None,
5332 }))
5333 .unwrap();
5334 assert!(engine.score.texts.is_empty());
5335 engine.undo().unwrap();
5336 assert_eq!(engine.score.texts.len(), 1);
5337 engine.redo().unwrap();
5338 assert!(engine.score.texts.is_empty());
5339 }
5340
5341 #[test]
5342 fn set_score_style_overrides_is_undoable_and_rejects_invalid_values() {
5343 let mut engine = crate::ScoreEngine::new();
5344 let overrides = vec![ViewStyleOverride {
5345 property: super::super::score::ViewStyleProperty::SystemGap,
5346 value: 2.5,
5347 }];
5348 engine
5349 .apply(Command::SetScoreStyleOverrides(SetScoreStyleOverridesCmd {
5350 overrides: overrides.clone(),
5351 }))
5352 .unwrap();
5353 assert_eq!(engine.score.style_overrides, overrides);
5354 engine.undo().unwrap();
5355 assert!(engine.score.style_overrides.is_empty());
5356 engine.redo().unwrap();
5357 assert_eq!(engine.score.style_overrides, overrides);
5358
5359 let before = engine.score.clone();
5360 assert!(
5361 engine
5362 .apply(Command::SetScoreStyleOverrides(SetScoreStyleOverridesCmd {
5363 overrides: vec![ViewStyleOverride {
5364 property: super::super::score::ViewStyleProperty::TextScale,
5365 value: f32::NAN,
5366 }],
5367 }))
5368 .is_err()
5369 );
5370 assert_eq!(
5371 serde_json::to_value(&engine.score).unwrap(),
5372 serde_json::to_value(&before).unwrap()
5373 );
5374 }
5375
5376 #[test]
5377 fn set_object_style_overrides_is_undoable_and_requires_existing_target() {
5378 let mut engine = crate::ScoreEngine::new();
5379 engine.score.texts.push(StyledText {
5380 style: crate::TextStyle::Expression,
5381 text: "Allegro".into(),
5382 placement: None,
5383 offset_x: None,
5384 offset_y: None,
5385 relative_x: None,
5386 relative_y: None,
5387 });
5388 let overrides = vec![ObjectStyleOverride {
5389 target: super::super::score::ObjectStyleTarget::ScoreText { text_index: 0 },
5390 property: super::super::score::ViewStyleProperty::TextScale,
5391 value: 1.2,
5392 provenance: None,
5393 }];
5394 engine
5395 .apply(Command::SetObjectStyleOverrides(
5396 SetObjectStyleOverridesCmd {
5397 overrides: overrides.clone(),
5398 },
5399 ))
5400 .unwrap();
5401 assert_eq!(engine.score.object_style_overrides, overrides);
5402 engine.undo().unwrap();
5403 assert!(engine.score.object_style_overrides.is_empty());
5404
5405 assert!(
5406 engine
5407 .apply(Command::SetObjectStyleOverrides(
5408 SetObjectStyleOverridesCmd {
5409 overrides: vec![ObjectStyleOverride {
5410 target: super::super::score::ObjectStyleTarget::ScoreText {
5411 text_index: 1,
5412 },
5413 property: super::super::score::ViewStyleProperty::TextScale,
5414 value: 1.2,
5415 provenance: None,
5416 }],
5417 }
5418 ))
5419 .is_err()
5420 );
5421 }
5422
5423 #[test]
5424 fn set_harmony_range_is_undoable_and_json_compatible() {
5425 let mut score = default_engine_score();
5426 score.parts[0].staves[0].measures[0].voices[0] =
5427 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
5428 score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
5429 root: "C".to_owned(),
5430 kind: "major".to_owned(),
5431 bass: None,
5432 placement: None,
5433 extender: true,
5434 harmonic_degree: None,
5435 harmony_function: None,
5436 harmony_type: None,
5437 chord_ref: None,
5438 range_end: None,
5439 degrees: Vec::new(),
5440 });
5441 let mut engine = ScoreEngine::new();
5442 engine.replace_score(score);
5443 let command = Command::SetHarmonyRange(SetHarmonyRangeCmd {
5444 part_index: 0,
5445 staff_index: 0,
5446 measure_index: 0,
5447 voice: 0,
5448 note_index: 0,
5449 end: Some(NoteAddr {
5450 part: 0,
5451 staff: 0,
5452 measure: 0,
5453 voice: 0,
5454 note: 0,
5455 }),
5456 });
5457 let json = serde_json::to_string(&command).unwrap();
5458 let restored: Command = serde_json::from_str(&json).unwrap();
5459 engine.apply(restored).unwrap();
5460 assert!(
5461 engine.score.parts[0].staves[0].measures[0].voices[0][0]
5462 .chord_symbol
5463 .as_ref()
5464 .and_then(|chord| chord.range_end.as_ref())
5465 .is_some()
5466 );
5467 engine.undo().unwrap();
5468 assert!(
5469 engine.score.parts[0].staves[0].measures[0].voices[0][0]
5470 .chord_symbol
5471 .as_ref()
5472 .is_some_and(|chord| chord.range_end.is_none())
5473 );
5474 }
5475
5476 #[test]
5477 fn set_harp_pedal_diagrams_is_undoable_and_json_compatible() {
5478 let mut engine = ScoreEngine::new();
5479 let mut diagram = HarpPedalDiagram::default();
5480 diagram.positions[0] = super::super::score::HarpPedalPosition::Flat;
5481 diagram.positions[6] = super::super::score::HarpPedalPosition::Sharp;
5482 let command = Command::SetHarpPedalDiagrams(SetHarpPedalDiagramsCmd {
5483 part_index: 0,
5484 staff_index: 0,
5485 measure_index: 0,
5486 diagrams: vec![diagram.clone()],
5487 });
5488 let restored: Command =
5489 serde_json::from_str(&serde_json::to_string(&command).unwrap()).unwrap();
5490 engine.apply(restored).unwrap();
5491 assert_eq!(
5492 engine.score.parts[0].staves[0].measures[0].harp_pedal_diagrams,
5493 vec![diagram]
5494 );
5495 engine.undo().unwrap();
5496 assert!(
5497 engine.score.parts[0].staves[0].measures[0]
5498 .harp_pedal_diagrams
5499 .is_empty()
5500 );
5501 }
5502
5503 #[test]
5504 fn set_note_placement_is_undoable_and_rejects_non_finite_values() {
5505 let mut score = default_engine_score();
5506 score.parts[0].staves[0].measures[0].voices[0] =
5507 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5508 let mut engine = ScoreEngine::new();
5509 engine.replace_score(score);
5510 let command = Command::SetNotePlacement(SetNotePlacementCmd {
5511 part_index: 0,
5512 staff_index: 0,
5513 measure_index: 0,
5514 voice: 0,
5515 note_index: 0,
5516 offset_x: Some(12.5),
5517 offset_y: Some(-3.0),
5518 relative_x: Some(1.25),
5519 relative_y: Some(-0.5),
5520 });
5521 engine.apply(command).unwrap();
5522 let note = &engine.score.parts[0].staves[0].measures[0].voices[0][0];
5523 assert_eq!(note.offset_x, Some(12.5));
5524 assert_eq!(note.relative_y, Some(-0.5));
5525 engine.undo().unwrap();
5526 assert_eq!(
5527 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
5528 None
5529 );
5530 let invalid = Command::SetNotePlacement(SetNotePlacementCmd {
5531 part_index: 0,
5532 staff_index: 0,
5533 measure_index: 0,
5534 voice: 0,
5535 note_index: 0,
5536 offset_x: Some(f64::NAN),
5537 offset_y: None,
5538 relative_x: None,
5539 relative_y: None,
5540 });
5541 assert!(engine.apply(invalid).is_err());
5542 assert_eq!(
5543 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
5544 None
5545 );
5546 }
5547
5548 #[test]
5549 fn set_guitar_bend_alter_updates_note_and_can_clear() {
5550 let mut score = default_engine_score();
5551 apply_command(
5552 &Command::AddNote(AddNoteCmd {
5553 part_index: 0,
5554 staff_index: 0,
5555 measure_index: 0,
5556 voice: 0,
5557 position: 0,
5558 pitch: Some(Pitch::new(Step::G, 4)),
5559 duration: Duration::Quarter,
5560 dot_count: 0,
5561 is_rest: false,
5562 tuplet: None,
5563 }),
5564 &mut score,
5565 )
5566 .unwrap();
5567 let location = SetGuitarBendAlterCmd {
5568 part_index: 0,
5569 staff_index: 0,
5570 measure_index: 0,
5571 voice: 0,
5572 note_index: 0,
5573 alter_cents: Some(200),
5574 };
5575 apply_command(&Command::SetGuitarBendAlter(location.clone()), &mut score).unwrap();
5576 assert_eq!(
5577 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
5578 Some(200)
5579 );
5580 let mut cleared = location;
5581 cleared.alter_cents = None;
5582 apply_command(&Command::SetGuitarBendAlter(cleared), &mut score).unwrap();
5583 assert_eq!(
5584 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
5585 None
5586 );
5587 }
5588
5589 #[test]
5590 fn set_guitar_bend_curve_is_undoable_and_json_compatible() {
5591 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5592 let command = Command::SetGuitarBendCurve(SetGuitarBendCurveCmd {
5593 part_index: 0,
5594 staff_index: 0,
5595 measure_index: 0,
5596 voice: 0,
5597 note_index: 0,
5598 points: vec![
5599 crate::GuitarBendPoint {
5600 position_per_mille: 0,
5601 alter_cents: 0,
5602 },
5603 crate::GuitarBendPoint {
5604 position_per_mille: 500,
5605 alter_cents: 200,
5606 },
5607 crate::GuitarBendPoint {
5608 position_per_mille: 1000,
5609 alter_cents: 0,
5610 },
5611 ],
5612 });
5613 let decoded: Command =
5614 serde_json::from_str(&serde_json::to_string(&command).unwrap()).unwrap();
5615 let mut stack = CommandStack::new(16);
5616 stack.execute(decoded, &mut score).unwrap();
5617 assert_eq!(
5618 score.parts[0].staves[0].measures[0].voices[0][0]
5619 .guitar_bend_curve
5620 .len(),
5621 3
5622 );
5623 stack.undo(&mut score).unwrap();
5624 assert!(
5625 score.parts[0].staves[0].measures[0].voices[0][0]
5626 .guitar_bend_curve
5627 .is_empty()
5628 );
5629 }
5630
5631 #[test]
5632 fn set_duration_updates_note_and_preserves_measure_capacity() {
5633 let mut score = default_engine_score();
5634 apply_command(
5635 &Command::AddNote(AddNoteCmd {
5636 part_index: 0,
5637 staff_index: 0,
5638 measure_index: 0,
5639 voice: 0,
5640 position: 0,
5641 pitch: Some(Pitch::new(Step::C, 4)),
5642 duration: Duration::Quarter,
5643 dot_count: 0,
5644 is_rest: false,
5645 tuplet: None,
5646 }),
5647 &mut score,
5648 )
5649 .unwrap();
5650 apply_command(
5651 &Command::SetDuration(SetDurationCmd {
5652 part_index: 0,
5653 staff_index: 0,
5654 measure_index: 0,
5655 voice: 0,
5656 note_index: 0,
5657 duration: Duration::Half,
5658 dot_count: 1,
5659 }),
5660 &mut score,
5661 )
5662 .unwrap();
5663 let voice = &score.parts[0].staves[0].measures[0].voices[0];
5664 assert_eq!(voice[0].duration, Duration::Half);
5665 assert_eq!(voice[0].dot_count, 1);
5666 assert!((voice.iter().map(|note| note.beats()).sum::<f64>() - 4.0).abs() < 1e-9);
5667 }
5668
5669 #[test]
5670 fn set_tempo_updates_score() {
5671 let mut score = default_engine_score();
5672 let cmd = Command::SetTempo(SetTempoCmd { bpm: 160 });
5673 apply_command(&cmd, &mut score).unwrap();
5674 assert_eq!(score.settings.tempo_bpm, 160);
5675 }
5676
5677 #[test]
5678 fn add_measure_increases_count() {
5679 let mut score = default_engine_score();
5680 let before = score.measure_count();
5681 apply_command(
5682 &Command::AddMeasure(AddMeasureCmd { after_index: 0 }),
5683 &mut score,
5684 )
5685 .unwrap();
5686 assert_eq!(score.measure_count(), before + 1);
5687 }
5688
5689 #[test]
5690 fn delete_measure_decreases_count() {
5691 let mut score = default_engine_score();
5692 let before = score.measure_count();
5693 apply_command(
5694 &Command::DeleteMeasure(DeleteMeasureCmd { measure_index: 0 }),
5695 &mut score,
5696 )
5697 .unwrap();
5698 assert_eq!(score.measure_count(), before - 1);
5699 }
5700
5701 #[test]
5702 fn undo_restores_score() {
5703 let mut stack = CommandStack::new(50);
5704 let mut score = default_engine_score();
5705 let before = score.settings.tempo_bpm;
5706 stack
5707 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
5708 .unwrap();
5709 assert_eq!(score.settings.tempo_bpm, 200);
5710 stack.undo(&mut score).unwrap();
5711 assert_eq!(score.settings.tempo_bpm, before);
5712 }
5713
5714 #[test]
5715 fn redo_reapplies_command() {
5716 let mut stack = CommandStack::new(50);
5717 let mut score = default_engine_score();
5718 stack
5719 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
5720 .unwrap();
5721 stack.undo(&mut score).unwrap();
5722 stack.redo(&mut score).unwrap();
5723 assert_eq!(score.settings.tempo_bpm, 200);
5724 }
5725
5726 #[test]
5727 fn undo_nothing_returns_error() {
5728 let mut stack = CommandStack::new(50);
5729 let mut score = default_engine_score();
5730 assert!(stack.undo(&mut score).is_err());
5731 }
5732
5733 #[test]
5734 fn add_part_appends_part() {
5735 let mut score = default_engine_score();
5736 let before = score.parts.len();
5737 apply_command(
5738 &Command::AddPart(AddPartCmd {
5739 name: "Violin".into(),
5740 short_name: "Vln.".into(),
5741 clefs: vec!["Treble".into()],
5742 midi_channel: 0,
5743 midi_program: 0,
5744 }),
5745 &mut score,
5746 )
5747 .unwrap();
5748 assert_eq!(score.parts.len(), before + 1);
5749 }
5750
5751 #[test]
5752 fn delete_part_removes_part() {
5753 let mut score = default_engine_score();
5754 apply_command(
5755 &Command::AddPart(AddPartCmd {
5756 name: "Violin".into(),
5757 short_name: "V.".into(),
5758 clefs: vec!["Treble".into()],
5759 midi_channel: 0,
5760 midi_program: 0,
5761 }),
5762 &mut score,
5763 )
5764 .unwrap();
5765 let before = score.parts.len();
5766 apply_command(
5767 &Command::DeletePart(DeletePartCmd { part_index: 0 }),
5768 &mut score,
5769 )
5770 .unwrap();
5771 assert_eq!(score.parts.len(), before - 1);
5772 }
5773
5774 #[test]
5775 fn delete_part_out_of_range_returns_err() {
5776 let mut score = default_engine_score();
5777 assert!(
5778 apply_command(
5779 &Command::DeletePart(DeletePartCmd { part_index: 99 }),
5780 &mut score
5781 )
5782 .is_err()
5783 );
5784 }
5785
5786 #[test]
5787 fn delete_part_undo_restores_part() {
5788 let mut stack = CommandStack::new(50);
5789 let mut score = default_engine_score();
5790 apply_command(
5791 &Command::AddPart(AddPartCmd {
5792 name: "Violin".into(),
5793 short_name: "V.".into(),
5794 clefs: vec!["Treble".into()],
5795 midi_channel: 0,
5796 midi_program: 0,
5797 }),
5798 &mut score,
5799 )
5800 .unwrap();
5801 let before = score.parts.len();
5802 stack
5803 .execute(
5804 Command::DeletePart(DeletePartCmd { part_index: 0 }),
5805 &mut score,
5806 )
5807 .unwrap();
5808 assert_eq!(score.parts.len(), before - 1);
5809 stack.undo(&mut score).unwrap();
5810 assert_eq!(score.parts.len(), before);
5811 }
5812
5813 #[test]
5814 fn reorder_parts_remaps_linked_views_and_preserves_undo() {
5815 let mut engine = crate::ScoreEngine::new();
5816 engine
5817 .apply(Command::AddPart(AddPartCmd {
5818 name: "Flute".into(),
5819 short_name: "Fl.".into(),
5820 clefs: vec!["Treble".into()],
5821 midi_channel: 1,
5822 midi_program: 73,
5823 }))
5824 .unwrap();
5825 engine
5826 .score
5827 .views
5828 .push(ScoreView::linked_part("flute", "Flute", 1));
5829 engine.score.part_groups.push(PartGroup {
5830 first_part: 0,
5831 last_part: 1,
5832 symbol: super::super::score::PartGroupSymbol::Bracket,
5833 barlines_connect: false,
5834 });
5835
5836 engine
5837 .apply(Command::ReorderParts(ReorderPartsCmd { order: vec![1, 0] }))
5838 .unwrap();
5839 assert_eq!(engine.score.parts[0].name, "Flute");
5840 assert_eq!(engine.score.views[0].parts, vec![0]);
5841 engine.undo().unwrap();
5842 assert_eq!(engine.score.parts[0].name, "Piano");
5843 assert_eq!(engine.score.views[0].parts, vec![1]);
5844 }
5845
5846 #[test]
5847 fn reorder_parts_rejects_splitting_a_part_group_without_mutation() {
5848 let mut score = Score::template(ScoreTemplate::StringQuartet);
5849 score.part_groups.push(PartGroup {
5850 first_part: 0,
5851 last_part: 1,
5852 symbol: super::super::score::PartGroupSymbol::Bracket,
5853 barlines_connect: false,
5854 });
5855 let before = serde_json::to_value(&score).unwrap();
5856
5857 assert!(
5858 apply_command(
5859 &Command::ReorderParts(ReorderPartsCmd {
5860 order: vec![0, 2, 1, 3],
5861 }),
5862 &mut score,
5863 )
5864 .is_err()
5865 );
5866 assert_eq!(serde_json::to_value(&score).unwrap(), before);
5867 }
5868
5869 #[test]
5870 fn set_metadata_updates_title() {
5871 let mut score = default_engine_score();
5872 apply_command(
5873 &Command::SetMetadata(SetMetadataCmd {
5874 title: Some("New Title".into()),
5875 ..Default::default()
5876 }),
5877 &mut score,
5878 )
5879 .unwrap();
5880 assert_eq!(score.metadata.title, "New Title");
5881 }
5882
5883 #[test]
5884 fn set_metadata_none_fields_skipped() {
5885 let mut score = default_engine_score();
5886 let original_composer = score.metadata.composer.clone();
5887 apply_command(
5888 &Command::SetMetadata(SetMetadataCmd {
5889 title: Some("X".into()),
5890 ..Default::default()
5891 }),
5892 &mut score,
5893 )
5894 .unwrap();
5895 assert_eq!(score.metadata.composer, original_composer);
5896 }
5897
5898 #[test]
5899 fn set_volta_sets_bracket() {
5900 use crate::model::score::VoltaBracket;
5901 let mut score = default_engine_score();
5902 let volta = VoltaBracket {
5903 number: 1,
5904 kind: "begin_end".into(),
5905 };
5906 apply_command(
5907 &Command::SetVolta(SetVoltaCmd {
5908 measure_index: 0,
5909 volta: Some(volta.clone()),
5910 }),
5911 &mut score,
5912 )
5913 .unwrap();
5914 assert!(score.parts[0].staves[0].measures[0].volta.is_some());
5915 }
5916
5917 #[test]
5918 fn set_volta_none_clears_bracket() {
5919 use crate::model::score::VoltaBracket;
5920 let mut score = default_engine_score();
5921 score.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
5922 number: 1,
5923 kind: "begin_end".into(),
5924 });
5925 apply_command(
5926 &Command::SetVolta(SetVoltaCmd {
5927 measure_index: 0,
5928 volta: None,
5929 }),
5930 &mut score,
5931 )
5932 .unwrap();
5933 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
5934 }
5935
5936 #[test]
5937 fn set_volta_undo_restores_old() {
5938 use crate::model::score::VoltaBracket;
5939 let mut stack = CommandStack::new(50);
5940 let mut score = default_engine_score();
5941 stack
5942 .execute(
5943 Command::SetVolta(SetVoltaCmd {
5944 measure_index: 0,
5945 volta: Some(VoltaBracket {
5946 number: 1,
5947 kind: "begin_end".into(),
5948 }),
5949 }),
5950 &mut score,
5951 )
5952 .unwrap();
5953 stack.undo(&mut score).unwrap();
5954 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
5955 }
5956
5957 #[test]
5958 fn set_clef_updates_staff_clef() {
5959 use crate::model::notation::Clef;
5960 let mut score = default_engine_score();
5961 apply_command(
5962 &Command::SetClef(SetClefCmd {
5963 part_index: 0,
5964 staff_index: 0,
5965 clef: Clef::Bass,
5966 }),
5967 &mut score,
5968 )
5969 .unwrap();
5970 assert_eq!(score.parts[0].staves[0].clef, Clef::Bass);
5971 }
5972
5973 #[test]
5974 fn set_clef_out_of_range_returns_err() {
5975 use crate::model::notation::Clef;
5976 let mut score = default_engine_score();
5977 assert!(
5978 apply_command(
5979 &Command::SetClef(SetClefCmd {
5980 part_index: 99,
5981 staff_index: 0,
5982 clef: Clef::Bass,
5983 }),
5984 &mut score
5985 )
5986 .is_err()
5987 );
5988 }
5989
5990 #[test]
5991 fn set_part_name_updates_name() {
5992 let mut score = default_engine_score();
5993 apply_command(
5994 &Command::SetPartName(SetPartNameCmd {
5995 part_index: 0,
5996 name: "Violin".into(),
5997 short_name: "Vln.".into(),
5998 }),
5999 &mut score,
6000 )
6001 .unwrap();
6002 assert_eq!(score.parts[0].name, "Violin");
6003 assert_eq!(score.parts[0].short_name, "Vln.");
6004 }
6005
6006 #[test]
6007 fn set_part_name_undo_restores_old() {
6008 let mut stack = CommandStack::new(50);
6009 let mut score = default_engine_score();
6010 let original = score.parts[0].name.clone();
6011 stack
6012 .execute(
6013 Command::SetPartName(SetPartNameCmd {
6014 part_index: 0,
6015 name: "Flute".into(),
6016 short_name: "Fl.".into(),
6017 }),
6018 &mut score,
6019 )
6020 .unwrap();
6021 stack.undo(&mut score).unwrap();
6022 assert_eq!(score.parts[0].name, original);
6023 }
6024
6025 #[test]
6026 fn set_metadata_undo_restores_old_title() {
6027 let mut stack = CommandStack::new(50);
6028 let mut score = default_engine_score();
6029 let original = score.metadata.title.clone();
6030 stack
6031 .execute(
6032 Command::SetMetadata(SetMetadataCmd {
6033 title: Some("Changed".into()),
6034 ..Default::default()
6035 }),
6036 &mut score,
6037 )
6038 .unwrap();
6039 assert_ne!(score.metadata.title, original);
6040 stack.undo(&mut score).unwrap();
6041 assert_eq!(score.metadata.title, original);
6042 }
6043
6044 #[test]
6045 fn set_midi_instrument_updates_channel_and_program() {
6046 let mut score = default_engine_score();
6047 apply_command(
6048 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
6049 part_index: 0,
6050 midi_channel: 2,
6051 midi_program: 40,
6052 }),
6053 &mut score,
6054 )
6055 .unwrap();
6056 assert_eq!(score.parts[0].midi_channel, 2);
6057 assert_eq!(score.parts[0].midi_program, 40);
6058 }
6059
6060 #[test]
6061 fn set_midi_instrument_clamps_channel_to_15() {
6062 let mut score = default_engine_score();
6063 apply_command(
6064 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
6065 part_index: 0,
6066 midi_channel: 20,
6067 midi_program: 0,
6068 }),
6069 &mut score,
6070 )
6071 .unwrap();
6072 assert_eq!(score.parts[0].midi_channel, 15);
6073 }
6074
6075 #[test]
6076 fn set_midi_instrument_undo_restores_old() {
6077 let mut stack = CommandStack::new(50);
6078 let mut score = default_engine_score();
6079 score.parts[0].midi_channel = 3;
6080 score.parts[0].midi_program = 10;
6081 stack
6082 .execute(
6083 Command::SetMidiInstrument(SetMidiInstrumentCmd {
6084 part_index: 0,
6085 midi_channel: 9,
6086 midi_program: 114,
6087 }),
6088 &mut score,
6089 )
6090 .unwrap();
6091 stack.undo(&mut score).unwrap();
6092 assert_eq!(score.parts[0].midi_channel, 3);
6093 assert_eq!(score.parts[0].midi_program, 10);
6094 }
6095
6096 #[test]
6097 fn set_transpose_updates_staff() {
6098 let mut score = default_engine_score();
6099 apply_command(
6100 &Command::SetTranspose(SetTransposeCmd {
6101 part_index: 0,
6102 staff_index: 0,
6103 semitones: -2,
6104 }),
6105 &mut score,
6106 )
6107 .unwrap();
6108 assert_eq!(score.parts[0].staves[0].transpose_semitones, -2);
6109 }
6110
6111 #[test]
6112 fn set_transpose_out_of_range_returns_err() {
6113 let mut score = default_engine_score();
6114 assert!(
6115 apply_command(
6116 &Command::SetTranspose(SetTransposeCmd {
6117 part_index: 99,
6118 staff_index: 0,
6119 semitones: -2,
6120 }),
6121 &mut score
6122 )
6123 .is_err()
6124 );
6125 }
6126
6127 #[test]
6128 fn transpose_staff_region_is_undoable_and_json_compatible() {
6129 let mut stack = CommandStack::new(50);
6130 let mut score = Score::new("Region", 120, 4, 4, 0, 2);
6131 for measure in &mut score.parts[0].staves[0].measures {
6132 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
6133 }
6134 let command = Command::TransposeStaffRegion(TransposeStaffRegionCmd {
6135 part_index: 0,
6136 staff_index: 0,
6137 start_measure: 1,
6138 end_measure: 2,
6139 semitones: 2,
6140 target: RegionalTranspositionTarget::Written,
6141 });
6142 let json = serde_json::to_string(&command).expect("command serializes");
6143 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6144 assert_eq!(command_key(&decoded), "TransposeStaffRegion");
6145 stack.execute(decoded, &mut score).expect("region applies");
6146 assert_eq!(
6147 score.parts[0].staves[0].measures[0].voices[0][0].pitches[0].to_midi(),
6148 60
6149 );
6150 assert_eq!(
6151 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0].to_midi(),
6152 62
6153 );
6154 stack.undo(&mut score).expect("region undo applies");
6155 assert_eq!(
6156 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0].to_midi(),
6157 60
6158 );
6159 }
6160
6161 #[test]
6162 fn set_tempo_at_measure_sets_tempo() {
6163 let mut score = default_engine_score();
6164 apply_command(
6165 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
6166 measure_index: 0,
6167 bpm: Some(80),
6168 }),
6169 &mut score,
6170 )
6171 .unwrap();
6172 assert_eq!(score.parts[0].staves[0].measures[0].tempo, Some(80));
6173 }
6174
6175 #[test]
6176 fn set_tempo_at_measure_none_clears_tempo() {
6177 let mut score = default_engine_score();
6178 score.parts[0].staves[0].measures[0].tempo = Some(120);
6179 apply_command(
6180 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
6181 measure_index: 0,
6182 bpm: None,
6183 }),
6184 &mut score,
6185 )
6186 .unwrap();
6187 assert!(score.parts[0].staves[0].measures[0].tempo.is_none());
6188 }
6189
6190 #[test]
6191 fn set_tempo_ramp_at_measure_applies() {
6192 let mut score = default_engine_score();
6193 apply_command(
6194 &Command::SetTempoRampAtMeasure(SetTempoRampAtMeasureCmd {
6195 measure_index: 0,
6196 target_bpm: Some(72),
6197 }),
6198 &mut score,
6199 )
6200 .expect("tempo ramp applies");
6201 assert_eq!(score.parts[0].staves[0].measures[0].tempo_ramp_to, Some(72));
6202 }
6203
6204 #[test]
6207 fn batch_execute_two_commands_single_undo() {
6208 let mut stack = CommandStack::new(50);
6209 let mut score = default_engine_score();
6210 let original_bpm = score.settings.tempo_bpm;
6211 stack
6212 .batch_execute(
6213 vec![
6214 Command::SetTempo(SetTempoCmd { bpm: 160 }),
6215 Command::SetTempo(SetTempoCmd { bpm: 180 }),
6216 ],
6217 &mut score,
6218 )
6219 .unwrap();
6220 assert_eq!(score.settings.tempo_bpm, 180);
6221 stack.undo(&mut score).unwrap();
6222 assert_eq!(score.settings.tempo_bpm, original_bpm);
6223 }
6224
6225 #[test]
6226 fn batch_execute_partial_failure_rollback() {
6227 let mut stack = CommandStack::new(50);
6228 let mut score = default_engine_score();
6229 let original_bpm = score.settings.tempo_bpm;
6230 let result = stack.batch_execute(
6231 vec![
6232 Command::SetTempo(SetTempoCmd { bpm: 160 }),
6233 Command::DeleteNote(DeleteNoteCmd {
6234 note_id: "nonexistent".into(),
6235 part_index: 99,
6236 staff_index: 0,
6237 measure_index: 0,
6238 voice: 0,
6239 }),
6240 ],
6241 &mut score,
6242 );
6243 assert!(result.is_err());
6244 assert_eq!(score.settings.tempo_bpm, original_bpm);
6245 }
6246
6247 #[test]
6248 fn execute_rejects_invalid_candidate_without_mutating_or_recording_history() {
6249 let mut stack = CommandStack::new(50);
6250 let mut score = default_engine_score();
6251 score.parts[0].staves[0].tablature = Some(crate::TablatureConfig {
6252 lines: 6,
6253 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6254 capo: 0,
6255 });
6256 stack
6257 .execute(
6258 Command::AddNote(AddNoteCmd {
6259 part_index: 0,
6260 staff_index: 0,
6261 measure_index: 0,
6262 voice: 0,
6263 position: 0,
6264 pitch: Some(Pitch::new(Step::C, 4)),
6265 duration: Duration::Quarter,
6266 dot_count: 0,
6267 is_rest: false,
6268 tuplet: None,
6269 }),
6270 &mut score,
6271 )
6272 .expect("valid pitched note should be accepted");
6273 let original = score.clone();
6274 let result = stack.execute(
6275 Command::SetTabPosition(SetTabPositionCmd {
6276 part_index: 0,
6277 staff_index: 0,
6278 measure_index: 0,
6279 voice: 0,
6280 note_index: 0,
6281 position: Some(crate::TabPosition { string: 7, fret: 0 }),
6282 }),
6283 &mut score,
6284 );
6285 assert!(matches!(result, Err(Error::InvalidScore)));
6286 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
6287 let original_note = &original.parts[0].staves[0].measures[0].voices[0][0];
6288 assert_eq!(note.pitches, original_note.pitches);
6289 assert_eq!(note.duration, original_note.duration);
6290 assert_eq!(note.tab_position, original_note.tab_position);
6291 assert_eq!(note.tab_positions, original_note.tab_positions);
6292 assert!(stack.can_undo());
6293 }
6294
6295 #[test]
6296 fn set_tablature_config_is_undoable_and_json_compatible() {
6297 let mut stack = CommandStack::new(50);
6298 let mut score = default_engine_score();
6299 let config = crate::TablatureConfig {
6300 lines: 6,
6301 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6302 capo: 2,
6303 };
6304 let command = Command::SetTablatureConfig(SetTablatureConfigCmd {
6305 part_index: 0,
6306 staff_index: 0,
6307 config: Some(config.clone()),
6308 });
6309 let json = serde_json::to_string(&command).expect("command should serialize");
6310 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6311 assert_eq!(command_key(&decoded), "SetTablatureConfig");
6312 stack
6313 .execute(decoded, &mut score)
6314 .expect("config should apply");
6315 assert_eq!(score.parts[0].staves[0].tablature, Some(config));
6316 assert_eq!(
6317 score.parts[0].staves[0].presentation.kind,
6318 StaffKind::Tablature
6319 );
6320 stack.undo(&mut score).expect("config undo should apply");
6321 assert!(score.parts[0].staves[0].tablature.is_none());
6322 assert_eq!(
6323 score.parts[0].staves[0].presentation.kind,
6324 StaffKind::Standard
6325 );
6326 stack.redo(&mut score).expect("config redo should apply");
6327 assert_eq!(
6328 score.parts[0].staves[0]
6329 .tablature
6330 .as_ref()
6331 .map(|tab| tab.capo),
6332 Some(2)
6333 );
6334 }
6335
6336 #[test]
6337 fn set_staff_presentation_is_undoable_and_requires_tablature_config() {
6338 let mut stack = CommandStack::new(50);
6339 let mut score = default_engine_score();
6340 let tab = StaffPresentation {
6341 kind: StaffKind::Tablature,
6342 lines: 6,
6343 line_distance: 1.25,
6344 small: true,
6345 cutaway: false,
6346 visible: true,
6347 notehead_scheme: super::super::score::StaffNoteheadScheme::Standard,
6348 tablature_rhythm_display: super::super::score::TablatureRhythmDisplay::FretOnly,
6349 tablature_fret_mark_style: super::super::score::TablatureFretMarkStyle::Arabic,
6350 };
6351 let rejected = Command::SetStaffPresentation(SetStaffPresentationCmd {
6352 part_index: 0,
6353 staff_index: 0,
6354 presentation: tab.clone(),
6355 });
6356 let before = score.clone();
6357 assert!(stack.execute(rejected, &mut score).is_err());
6358 assert_eq!(
6359 score.parts[0].staves[0].presentation,
6360 before.parts[0].staves[0].presentation
6361 );
6362 assert!(!stack.can_undo());
6363
6364 stack
6365 .execute(
6366 Command::SetTablatureConfig(SetTablatureConfigCmd {
6367 part_index: 0,
6368 staff_index: 0,
6369 config: Some(crate::TablatureConfig {
6370 lines: 6,
6371 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6372 capo: 0,
6373 }),
6374 }),
6375 &mut score,
6376 )
6377 .expect("tablature configuration should apply");
6378 stack
6379 .execute(
6380 Command::SetStaffPresentation(SetStaffPresentationCmd {
6381 part_index: 0,
6382 staff_index: 0,
6383 presentation: tab.clone(),
6384 }),
6385 &mut score,
6386 )
6387 .expect("staff presentation should apply");
6388 assert_eq!(stack.undo_key().as_deref(), Some("SetStaffPresentation"));
6389 assert_eq!(score.parts[0].staves[0].presentation, tab);
6390 stack
6391 .undo(&mut score)
6392 .expect("presentation undo should apply");
6393 assert_eq!(
6394 score.parts[0].staves[0].presentation.kind,
6395 StaffKind::Tablature
6396 );
6397 stack
6398 .redo(&mut score)
6399 .expect("presentation redo should apply");
6400 assert_eq!(
6401 score.parts[0].staves[0].presentation.kind,
6402 StaffKind::Tablature
6403 );
6404 }
6405
6406 #[test]
6407 fn set_instrument_definition_is_undoable_and_rejects_invalid_ranges() {
6408 let mut stack = CommandStack::new(50);
6409 let mut score = default_engine_score();
6410 let mut invalid = InstrumentDefinition::new("violin", "Violin");
6411 invalid.written_range = Some(InstrumentRange {
6412 lowest: 100,
6413 highest: 55,
6414 });
6415 let before = score.clone();
6416 assert!(
6417 stack
6418 .execute(
6419 Command::SetInstrumentDefinition(SetInstrumentDefinitionCmd {
6420 part_index: 0,
6421 definition: Some(invalid),
6422 }),
6423 &mut score,
6424 )
6425 .is_err()
6426 );
6427 assert_eq!(score.parts[0].instrument, before.parts[0].instrument);
6428
6429 let mut definition = InstrumentDefinition::new("violin", "Violin");
6430 definition.short_name = "Vln.".to_string();
6431 definition.family = Some("strings".to_string());
6432 definition.written_range = Some(InstrumentRange {
6433 lowest: 55,
6434 highest: 103,
6435 });
6436 definition.sounding_range = definition.written_range;
6437 definition.default_clefs = vec![Clef::Treble];
6438 definition.midi_program = 40;
6439 let command = Command::SetInstrumentDefinition(SetInstrumentDefinitionCmd {
6440 part_index: 0,
6441 definition: Some(definition.clone()),
6442 });
6443 let json = serde_json::to_string(&command).expect("command should serialize");
6444 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6445 assert_eq!(command_key(&decoded), "SetInstrumentDefinition");
6446 stack
6447 .execute(decoded, &mut score)
6448 .expect("definition applies");
6449 assert_eq!(score.parts[0].instrument, Some(definition));
6450 stack.undo(&mut score).expect("definition undo applies");
6451 assert!(score.parts[0].instrument.is_none());
6452 stack.redo(&mut score).expect("definition redo applies");
6453 assert_eq!(
6454 score.parts[0]
6455 .instrument
6456 .as_ref()
6457 .map(|value| value.id.as_str()),
6458 Some("violin")
6459 );
6460 }
6461
6462 #[test]
6463 fn set_percussion_kit_is_undoable_and_rejects_invalid_entries() {
6464 let mut stack = CommandStack::new(50);
6465 let mut score = default_engine_score();
6466 let invalid = Command::SetPercussionKit(SetPercussionKitCmd {
6467 part_index: 0,
6468 instruments: vec![PercussionInstrument {
6469 id: "snare".to_string(),
6470 name: None,
6471 midi_unpitched: Some(38),
6472 staff_position: Some(33),
6473 notehead: None,
6474 preferred_voice: None,
6475 techniques: Vec::new(),
6476 }],
6477 });
6478 assert!(stack.execute(invalid, &mut score).is_err());
6479 assert!(score.parts[0].percussion_instruments.is_empty());
6480
6481 let kit = vec![PercussionInstrument {
6482 id: "snare".to_string(),
6483 name: Some("Acoustic Snare".to_string()),
6484 midi_unpitched: Some(38),
6485 staff_position: Some(0),
6486 notehead: Some(NoteHead::Cross),
6487 preferred_voice: Some(1),
6488 techniques: vec!["rim-shot".to_string()],
6489 }];
6490 let command = Command::SetPercussionKit(SetPercussionKitCmd {
6491 part_index: 0,
6492 instruments: kit.clone(),
6493 });
6494 let json = serde_json::to_string(&command).expect("command serializes");
6495 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6496 assert_eq!(command_key(&decoded), "SetPercussionKit");
6497 stack.execute(decoded, &mut score).expect("kit applies");
6498 assert_eq!(score.parts[0].percussion_instruments, kit);
6499 stack.undo(&mut score).expect("kit undo applies");
6500 assert!(score.parts[0].percussion_instruments.is_empty());
6501 stack.redo(&mut score).expect("kit redo applies");
6502 assert_eq!(score.parts[0].percussion_instruments, kit);
6503 }
6504
6505 #[test]
6506 fn set_measure_instrument_change_is_undoable_and_json_compatible() {
6507 let mut stack = CommandStack::new(50);
6508 let mut score = default_engine_score();
6509 let mut definition = InstrumentDefinition::new("clarinet-bb", "B-flat Clarinet");
6510 definition.transpose_semitones = -2;
6511 definition.midi_program = 71;
6512 let command = Command::SetMeasureInstrumentChange(SetMeasureInstrumentChangeCmd {
6513 part_index: 0,
6514 staff_index: 0,
6515 measure_index: 2,
6516 definition: Some(definition.clone()),
6517 });
6518 let json = serde_json::to_string(&command).expect("command should serialize");
6519 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6520 assert_eq!(command_key(&decoded), "SetMeasureInstrumentChange");
6521 stack.execute(decoded, &mut score).expect("change applies");
6522 assert_eq!(
6523 score.parts[0].staves[0].measures[2].instrument_change,
6524 Some(definition)
6525 );
6526 stack.undo(&mut score).expect("undo applies");
6527 assert!(
6528 score.parts[0].staves[0].measures[2]
6529 .instrument_change
6530 .is_none()
6531 );
6532 }
6533
6534 #[test]
6535 fn set_measure_tablature_change_is_undoable_and_uses_base_line_count() {
6536 let mut stack = CommandStack::new(50);
6537 let mut score = default_engine_score();
6538 let base = crate::TablatureConfig {
6539 lines: 6,
6540 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6541 capo: 0,
6542 };
6543 score.parts[0].staves[0].tablature = Some(base.clone());
6544 let mut changed = base;
6545 changed.tuning_midi[0] = 38;
6546 changed.capo = 2;
6547 let command = Command::SetMeasureTablatureChange(SetMeasureTablatureChangeCmd {
6548 part_index: 0,
6549 staff_index: 0,
6550 measure_index: 2,
6551 config: Some(changed.clone()),
6552 });
6553 let json = serde_json::to_string(&command).expect("command serializes");
6554 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6555 assert_eq!(command_key(&decoded), "SetMeasureTablatureChange");
6556 stack.execute(decoded, &mut score).expect("change applies");
6557 assert_eq!(
6558 score.parts[0].staves[0].tablature_at(1),
6559 score.parts[0].staves[0].tablature
6560 );
6561 assert_eq!(score.parts[0].staves[0].tablature_at(2), Some(changed));
6562 stack.undo(&mut score).expect("change undo applies");
6563 assert!(
6564 score.parts[0].staves[0].measures[2]
6565 .tablature_change
6566 .is_none()
6567 );
6568
6569 let invalid = Command::SetMeasureTablatureChange(SetMeasureTablatureChangeCmd {
6570 part_index: 0,
6571 staff_index: 0,
6572 measure_index: 1,
6573 config: Some(crate::TablatureConfig {
6574 lines: 7,
6575 tuning_midi: vec![40, 45, 50, 55, 59, 64, 69],
6576 capo: 0,
6577 }),
6578 });
6579 assert!(stack.execute(invalid, &mut score).is_err());
6580 }
6581
6582 #[test]
6583 fn batch_execute_empty_is_noop() {
6584 let mut stack = CommandStack::new(50);
6585 let mut score = default_engine_score();
6586 stack.batch_execute(vec![], &mut score).unwrap();
6587 assert!(!stack.can_undo());
6588 }
6589
6590 #[test]
6593 fn batch_label_used_as_command_key() {
6594 let cmd = Command::Batch(BatchCmd {
6595 commands: vec![],
6596 label: Some("ApplyAI".to_string()),
6597 });
6598 assert_eq!(command_key(&cmd), "ApplyAI");
6599 }
6600
6601 #[test]
6602 fn batch_no_label_key_is_batch() {
6603 let cmd = Command::Batch(BatchCmd {
6604 commands: vec![],
6605 label: None,
6606 });
6607 assert_eq!(command_key(&cmd), "Batch");
6608 }
6609
6610 #[test]
6611 fn batch_label_survives_json_roundtrip() {
6612 let cmd = Command::Batch(BatchCmd {
6613 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 120 })],
6614 label: Some("PasteSelection".to_string()),
6615 });
6616 let json = serde_json::to_string(&cmd).unwrap();
6617 let cmd2: Command = serde_json::from_str(&json).unwrap();
6618 assert_eq!(command_key(&cmd2), "PasteSelection");
6619 }
6620
6621 #[test]
6622 fn batch_label_in_undo_key() {
6623 let mut stack = CommandStack::new(50);
6624 let mut score = default_engine_score();
6625 let cmd = Command::Batch(BatchCmd {
6626 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 140 })],
6627 label: Some("ApplyAI".to_string()),
6628 });
6629 stack.execute(cmd, &mut score).unwrap();
6630 assert_eq!(stack.undo_key(), Some("ApplyAI".to_string()));
6631 }
6632
6633 #[test]
6634 fn undo_returns_change_hint() {
6635 use crate::model::change_hint::ChangeScope;
6636 let mut stack = CommandStack::new(50);
6637 let mut score = default_engine_score();
6638 stack
6639 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
6640 .unwrap();
6641 let hint = stack.undo(&mut score).unwrap();
6642 assert_eq!(hint.scope, ChangeScope::Global);
6643 assert!(hint.playback_dirty);
6644 }
6645
6646 #[test]
6647 fn redo_returns_change_hint() {
6648 use crate::model::change_hint::ChangeScope;
6649 let mut stack = CommandStack::new(50);
6650 let mut score = default_engine_score();
6651 stack
6652 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
6653 .unwrap();
6654 stack.undo(&mut score).unwrap();
6655 let hint = stack.redo(&mut score).unwrap();
6656 assert_eq!(hint.scope, ChangeScope::Global);
6657 assert!(hint.playback_dirty);
6658 }
6659
6660 #[test]
6663 fn toggle_slur_sets_start_and_end() {
6664 let mut score = default_engine_score();
6665 let cmd = Command::AddNote(AddNoteCmd {
6666 part_index: 0,
6667 staff_index: 0,
6668 measure_index: 0,
6669 voice: 0,
6670 position: 0,
6671 pitch: Some(Pitch::new(Step::C, 4)),
6672 duration: Duration::Quarter,
6673 dot_count: 0,
6674 is_rest: false,
6675 tuplet: None,
6676 });
6677 apply_command(&cmd, &mut score).unwrap();
6678 apply_command(
6679 &Command::AddNote(AddNoteCmd {
6680 part_index: 0,
6681 staff_index: 0,
6682 measure_index: 0,
6683 voice: 0,
6684 position: 1,
6685 pitch: Some(Pitch::new(Step::D, 4)),
6686 duration: Duration::Quarter,
6687 dot_count: 0,
6688 is_rest: false,
6689 tuplet: None,
6690 }),
6691 &mut score,
6692 )
6693 .unwrap();
6694 let start = NoteAddr {
6695 part: 0,
6696 staff: 0,
6697 measure: 0,
6698 voice: 0,
6699 note: 0,
6700 };
6701 let end = NoteAddr {
6702 part: 0,
6703 staff: 0,
6704 measure: 0,
6705 voice: 0,
6706 note: 1,
6707 };
6708 apply_command(
6709 &Command::ToggleSlur(ToggleSlurCmd {
6710 start: start.clone(),
6711 end: end.clone(),
6712 }),
6713 &mut score,
6714 )
6715 .unwrap();
6716 assert!(score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
6717 assert!(score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
6718 apply_command(
6720 &Command::ToggleSlur(ToggleSlurCmd { start, end }),
6721 &mut score,
6722 )
6723 .unwrap();
6724 assert!(!score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
6725 assert!(!score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
6726 }
6727
6728 #[test]
6731 fn add_staff_appends_staff_with_correct_measure_count() {
6732 let mut score = default_engine_score();
6733 let before = score.parts[0].staves.len();
6734 let measure_count = score.parts[0].staves[0].measures.len();
6735 apply_command(
6736 &Command::AddStaff(AddStaffCmd {
6737 part_index: 0,
6738 clef: Clef::Bass,
6739 }),
6740 &mut score,
6741 )
6742 .unwrap();
6743 assert_eq!(score.parts[0].staves.len(), before + 1);
6744 let new_staff = score.parts[0].staves.last().unwrap();
6745 assert_eq!(new_staff.measures.len(), measure_count);
6746 }
6747
6748 #[test]
6749 fn add_staff_out_of_range_returns_err() {
6750 let mut score = default_engine_score();
6751 let result = apply_command(
6752 &Command::AddStaff(AddStaffCmd {
6753 part_index: 99,
6754 clef: Clef::Treble,
6755 }),
6756 &mut score,
6757 );
6758 assert!(result.is_err());
6759 }
6760
6761 #[test]
6762 fn delete_staff_removes_extra_staff() {
6763 let mut score = default_engine_score();
6764 apply_command(
6765 &Command::AddStaff(AddStaffCmd {
6766 part_index: 0,
6767 clef: Clef::Bass,
6768 }),
6769 &mut score,
6770 )
6771 .unwrap();
6772 assert_eq!(score.parts[0].staves.len(), 2);
6773 apply_command(
6774 &Command::DeleteStaff(DeleteStaffCmd {
6775 part_index: 0,
6776 staff_index: 1,
6777 }),
6778 &mut score,
6779 )
6780 .unwrap();
6781 assert_eq!(score.parts[0].staves.len(), 1);
6782 }
6783
6784 #[test]
6785 fn delete_last_staff_returns_err() {
6786 let mut score = default_engine_score();
6787 assert_eq!(score.parts[0].staves.len(), 1);
6788 let result = apply_command(
6789 &Command::DeleteStaff(DeleteStaffCmd {
6790 part_index: 0,
6791 staff_index: 0,
6792 }),
6793 &mut score,
6794 );
6795 assert!(result.is_err());
6796 }
6797
6798 #[test]
6801 fn set_tuplet_assigns_and_clears() {
6802 use crate::model::notation::TupletInfo;
6803 let mut score = default_engine_score();
6804 apply_command(
6805 &Command::AddNote(AddNoteCmd {
6806 part_index: 0,
6807 staff_index: 0,
6808 measure_index: 0,
6809 voice: 0,
6810 position: 0,
6811 pitch: Some(Pitch::new(Step::C, 4)),
6812 duration: Duration::Quarter,
6813 dot_count: 0,
6814 is_rest: false,
6815 tuplet: None,
6816 }),
6817 &mut score,
6818 )
6819 .unwrap();
6820 let ti = TupletInfo {
6821 actual_notes: 3,
6822 normal_notes: 2,
6823 };
6824 apply_command(
6825 &Command::SetTuplet(SetTupletCmd {
6826 part_index: 0,
6827 staff_index: 0,
6828 measure_index: 0,
6829 voice_index: 0,
6830 note_index: 0,
6831 tuplet: Some(ti.clone()),
6832 }),
6833 &mut score,
6834 )
6835 .unwrap();
6836 assert_eq!(
6837 score.parts[0].staves[0].measures[0].voices[0][0].tuplet,
6838 Some(ti)
6839 );
6840 apply_command(
6841 &Command::SetTuplet(SetTupletCmd {
6842 part_index: 0,
6843 staff_index: 0,
6844 measure_index: 0,
6845 voice_index: 0,
6846 note_index: 0,
6847 tuplet: None,
6848 }),
6849 &mut score,
6850 )
6851 .unwrap();
6852 assert!(
6853 score.parts[0].staves[0].measures[0].voices[0][0]
6854 .tuplet
6855 .is_none()
6856 );
6857 }
6858
6859 #[test]
6862 fn respell_score_cmd_changes_all_pitches() {
6863 use crate::model::pitch::Step;
6864 let mut score = default_engine_score();
6865 apply_command(
6866 &Command::AddNote(AddNoteCmd {
6867 part_index: 0,
6868 staff_index: 0,
6869 measure_index: 0,
6870 voice: 0,
6871 position: 0,
6872 pitch: Some(Pitch::with_alter(Step::C, 4, 1)), duration: Duration::Quarter,
6874 dot_count: 0,
6875 is_rest: false,
6876 tuplet: None,
6877 }),
6878 &mut score,
6879 )
6880 .unwrap();
6881 apply_command(
6882 &Command::RespellScore(RespellScoreCmd { prefer_flat: true }),
6883 &mut score,
6884 )
6885 .unwrap();
6886 let pitch = &score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
6887 assert_eq!(pitch.step, Step::D);
6888 assert_eq!(pitch.alter, -1); }
6890
6891 fn respell_fixture(measures: u32) -> Score {
6892 use crate::model::pitch::Step;
6893 let mut score = Score::new("Respell", 120, 4, 4, 0, measures);
6894 for measure in &mut score.parts[0].staves[0].measures {
6895 measure.voices[0] = vec![Note::new(Pitch::with_alter(Step::C, 4, 1), Duration::Whole)];
6896 }
6897 score
6898 }
6899
6900 fn spelled(score: &Score, measure: usize) -> (crate::model::pitch::Step, i8) {
6901 let pitch = &score.parts[0].staves[0].measures[measure].voices[0][0].pitches[0];
6902 (pitch.step.clone(), pitch.alter)
6903 }
6904
6905 #[test]
6906 fn respell_staff_region_changes_only_the_selection_and_is_undoable() {
6907 use crate::model::pitch::Step;
6908 let mut score = respell_fixture(4);
6909 let mut stack = CommandStack::new(50);
6910 let command = Command::RespellStaffRegion(RespellStaffRegionCmd {
6911 part_index: 0,
6912 staff_index: 0,
6913 start_measure: 1,
6914 end_measure: 3,
6915 policy: RespellPolicy::Flat,
6916 });
6917 let json = serde_json::to_string(&command).expect("command serializes");
6918 assert!(json.contains("\"policy\":\"flat\""));
6919 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6920 assert_eq!(command_key(&decoded), "RespellStaffRegion");
6921 assert_eq!(command_label(&decoded), "Respell Pitches (flat)");
6922 stack.execute(decoded, &mut score).expect("region respells");
6923 assert_eq!(spelled(&score, 0), (Step::C, 1));
6924 assert_eq!(spelled(&score, 1), (Step::D, -1));
6925 assert_eq!(spelled(&score, 2), (Step::D, -1));
6926 assert_eq!(spelled(&score, 3), (Step::C, 1));
6927 stack.undo(&mut score).expect("region respell undoes");
6928 assert!((0..4).all(|measure| spelled(&score, measure) == (Step::C, 1)));
6929 stack.redo(&mut score).expect("region respell redoes");
6930 assert_eq!(spelled(&score, 2), (Step::D, -1));
6931 }
6932
6933 #[test]
6934 fn respell_staff_region_keeps_ties_across_the_boundary_in_one_spelling() {
6935 use crate::model::pitch::Step;
6936 let mut score = respell_fixture(4);
6937 {
6938 let measures = &mut score.parts[0].staves[0].measures;
6939 measures[0].voices[0][0].tie_start = true;
6940 measures[1].voices[0][0].tie_end = true;
6941 measures[1].voices[0][0].tie_start = true;
6942 measures[2].voices[0][0].tie_end = true;
6943 }
6944 let changed = respell_staff_region(&mut score, 0, 0, 1, 2, RespellPolicy::Flat)
6945 .expect("tied region respells");
6946 assert_eq!(changed, (0, 2));
6947 assert_eq!(spelled(&score, 0), (Step::D, -1));
6948 assert_eq!(spelled(&score, 1), (Step::D, -1));
6949 assert_eq!(spelled(&score, 2), (Step::D, -1));
6950 assert_eq!(spelled(&score, 3), (Step::C, 1));
6951 }
6952
6953 #[test]
6954 fn respell_staff_region_follows_local_keys_and_skips_unpitched_notes() {
6955 use crate::model::notation::KeySignature;
6956 use crate::model::pitch::Step;
6957 let mut score = respell_fixture(4);
6958 for measure in &mut score.parts[0].staves[0].measures {
6959 measure.voices[0][0].pitches[0] = Pitch::with_alter(Step::D, 4, -1);
6960 }
6961 score.parts[0].staves[0].measures[2].key_sig = Some(KeySignature {
6962 fifths: -3,
6963 mode: "major".into(),
6964 });
6965 score.parts[0].staves[0].measures[1].voices[0][0].is_unpitched = true;
6966 respell_staff_region(&mut score, 0, 0, 0, 4, RespellPolicy::Key).expect("key respell");
6967 assert_eq!(spelled(&score, 0), (Step::C, 1));
6968 assert_eq!(
6969 spelled(&score, 1),
6970 (Step::D, -1),
6971 "unpitched staff position is kept"
6972 );
6973 assert_eq!(spelled(&score, 2), (Step::D, -1));
6974 assert_eq!(spelled(&score, 3), (Step::D, -1));
6975
6976 assert!(matches!(
6977 respell_staff_region(&mut score, 0, 0, 2, 2, RespellPolicy::Sharp),
6978 Err(Error::InvalidCommand(_))
6979 ));
6980 assert!(matches!(
6981 respell_staff_region(&mut score, 0, 5, 0, 1, RespellPolicy::Sharp),
6982 Err(Error::StaffNotFound(5))
6983 ));
6984 assert!(matches!(
6985 respell_staff_region(&mut score, 0, 0, 0, 5, RespellPolicy::Sharp),
6986 Err(Error::InvalidCommand(_))
6987 ));
6988 }
6989
6990 #[test]
6991 fn pickup_measure_length_limits_editing_and_survives_time_signature_changes() {
6992 use crate::model::score::MeasureLength;
6993 let mut score = Score::new("Pickup", 120, 4, 4, 0, 2);
6994 score.parts[0].staves[0].measures[0].actual_length = Some(MeasureLength {
6995 numerator: 1,
6996 denominator: 4,
6997 });
6998 score.parts[0].staves[0].measures[0].voices[0] = vec![Note::rest(Duration::Quarter)];
6999 assert!(validate(&score).is_valid());
7000 assert_eq!(
7001 crate::measure_beats_remaining(&score, 0, 0, 0, 0).expect("capacity"),
7002 0.0
7003 );
7004 let mut stack = CommandStack::new(50);
7005 stack
7006 .execute(
7007 Command::AddNote(AddNoteCmd {
7008 part_index: 0,
7009 staff_index: 0,
7010 measure_index: 0,
7011 voice: 0,
7012 position: 0,
7013 pitch: Some(Pitch::new(Step::G, 4)),
7014 duration: Duration::Quarter,
7015 dot_count: 0,
7016 is_rest: false,
7017 tuplet: None,
7018 }),
7019 &mut score,
7020 )
7021 .expect("note fits the pickup after trimming");
7022 let pickup = &score.parts[0].staves[0].measures[0].voices[0];
7023 assert_eq!(pickup.len(), 1);
7024 assert!(!pickup[0].is_rest);
7025
7026 stack
7027 .execute(
7028 Command::SetTimeSignature(SetTimeSignatureCmd {
7029 numerator: 3,
7030 denominator: 4,
7031 }),
7032 &mut score,
7033 )
7034 .expect("time signature changes");
7035 let beats: f64 = score.parts[0].staves[0].measures[0].voices[0]
7036 .iter()
7037 .map(|note| note.beats())
7038 .sum();
7039 assert!((beats - 1.0).abs() < 1e-9, "the pickup keeps one beat");
7040
7041 score.parts[0].staves[0].measures[1].actual_length = Some(MeasureLength {
7042 numerator: 0,
7043 denominator: 4,
7044 });
7045 assert!(validate(&score).errors.iter().any(|error| matches!(
7046 error,
7047 crate::ValidationError::InvalidMeasureLength { measure: 1, .. }
7048 )));
7049 }
7050
7051 #[test]
7052 fn set_lyric_addresses_verses_without_touching_verse_one() {
7053 use crate::model::notation::VerseLyric;
7054 let mut score = Score::new("Verses", 120, 4, 4, 0, 1);
7055 score.parts[0].staves[0].measures[0].voices[0] =
7056 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
7057 let lyric = |text: &str| Lyric {
7058 text: text.into(),
7059 syllabic: "single".into(),
7060 };
7061 let set = |verse: Option<u8>, text: Option<&str>| {
7062 Command::SetLyric(SetLyricCmd {
7063 part_index: 0,
7064 staff_index: 0,
7065 measure_index: 0,
7066 voice: 0,
7067 note_index: 0,
7068 lyric: text.map(lyric),
7069 verse,
7070 })
7071 };
7072 let mut stack = CommandStack::new(50);
7073 stack
7074 .execute(set(None, Some("one")), &mut score)
7075 .expect("verse 1");
7076 stack
7077 .execute(set(Some(3), Some("three")), &mut score)
7078 .expect("verse 3");
7079 stack
7080 .execute(set(Some(2), Some("two")), &mut score)
7081 .expect("verse 2");
7082 stack
7083 .execute(set(Some(2), Some("TWO")), &mut score)
7084 .expect("replace verse 2");
7085 let note = |score: &Score| score.parts[0].staves[0].measures[0].voices[0][0].clone();
7086 assert_eq!(note(&score).lyric, Some(lyric("one")));
7087 assert_eq!(
7088 note(&score).additional_lyrics,
7089 vec![
7090 VerseLyric {
7091 verse: 2,
7092 lyric: lyric("TWO")
7093 },
7094 VerseLyric {
7095 verse: 3,
7096 lyric: lyric("three")
7097 }
7098 ]
7099 );
7100 stack
7101 .execute(set(Some(3), None), &mut score)
7102 .expect("clear verse 3");
7103 assert_eq!(note(&score).additional_lyrics.len(), 1);
7104 stack.undo(&mut score).expect("clear undoes");
7105 assert_eq!(note(&score).additional_lyrics.len(), 2);
7106
7107 let current = serde_json::to_string(&set(None, None)).expect("SetLyric serializes");
7109 assert!(current.contains("\"verse\":null"));
7110 let legacy: Command = serde_json::from_str(¤t.replace(",\"verse\":null", ""))
7111 .expect("legacy SetLyric JSON");
7112 stack
7113 .execute(legacy, &mut score)
7114 .expect("legacy clears verse 1");
7115 assert_eq!(note(&score).lyric, None);
7116 assert_eq!(note(&score).additional_lyrics.len(), 2);
7117
7118 assert!(matches!(
7119 apply_command(&set(Some(0), Some("zero")), &mut score),
7120 Err(Error::InvalidCommand(_))
7121 ));
7122 assert!(matches!(
7123 apply_command(&set(Some(33), Some("too far")), &mut score),
7124 Err(Error::InvalidCommand(_))
7125 ));
7126 score.parts[0].staves[0].measures[0].voices[0][0]
7127 .additional_lyrics
7128 .push(VerseLyric {
7129 verse: 2,
7130 lyric: lyric("duplicate"),
7131 });
7132 assert!(validate(&score).errors.iter().any(|error| matches!(
7133 error,
7134 crate::ValidationError::InvalidLyricVerse { verse: 2, .. }
7135 )));
7136 }
7137
7138 #[test]
7139 fn cycle_enharmonic_spelling_targets_one_chord_member_and_undoes() {
7140 let mut score = Score::new("Enharmonic", 120, 4, 4, 0, 1);
7141 let mut chord = Note::new(Pitch::with_alter(Step::C, 4, 1), Duration::Whole);
7142 chord.pitches.push(Pitch::with_alter(Step::G, 4, 1));
7143 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
7144 let mut stack = CommandStack::new(50);
7145 let command = Command::CycleEnharmonicSpelling(CycleEnharmonicSpellingCmd {
7146 part_index: 0,
7147 staff_index: 0,
7148 measure_index: 0,
7149 voice: 0,
7150 note_index: 0,
7151 pitch_index: Some(1),
7152 });
7153 let json = serde_json::to_string(&command).expect("command serializes");
7154 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7155 assert_eq!(command_key(&decoded), "CycleEnharmonicSpelling");
7156 stack.execute(decoded, &mut score).expect("spelling cycles");
7157 let pitches = |score: &Score| {
7158 score.parts[0].staves[0].measures[0].voices[0][0]
7159 .pitches
7160 .clone()
7161 };
7162 assert_eq!(
7163 pitches(&score),
7164 vec![
7165 Pitch::with_alter(Step::C, 4, 1),
7166 Pitch::with_alter(Step::A, 4, -1)
7167 ]
7168 );
7169 stack.undo(&mut score).expect("spelling undoes");
7170 assert_eq!(pitches(&score)[1], Pitch::with_alter(Step::G, 4, 1));
7171
7172 let mut rest_score = Score::new("Rest", 120, 4, 4, 0, 1);
7173 rest_score.parts[0].staves[0].measures[0].voices[0] = vec![Note::rest(Duration::Whole)];
7174 assert!(matches!(
7175 apply_command(&command, &mut rest_score),
7176 Err(Error::InvalidCommand(_))
7177 ));
7178 }
7179
7180 #[test]
7181 fn resequence_rehearsal_marks_continues_the_first_sequence() {
7182 let mut score = Score::new("Marks", 120, 4, 4, 0, 30);
7183 let mut stack = CommandStack::new(50);
7184 for (index, text) in [(0, "A"), (4, "C"), (9, "C"), (12, "Q")] {
7185 for_each_measure_at(&mut score, index, |measure| {
7186 measure.rehearsal = Some(text.into())
7187 });
7188 }
7189 let command = Command::ResequenceRehearsalMarks(ResequenceRehearsalMarksCmd::default());
7190 let json = serde_json::to_string(&command).expect("command serializes");
7191 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7192 assert_eq!(command_key(&decoded), "ResequenceRehearsalMarks");
7193 stack
7194 .execute(decoded, &mut score)
7195 .expect("marks resequence");
7196 let marks = |score: &Score| -> Vec<Option<String>> {
7197 [0, 4, 9, 12]
7198 .iter()
7199 .map(|&index| score.parts[0].staves[0].measures[index].rehearsal.clone())
7200 .collect()
7201 };
7202 assert_eq!(
7203 marks(&score),
7204 ["A", "B", "C", "D"].map(|text| Some(text.to_string()))
7205 );
7206 stack.undo(&mut score).expect("resequence undoes");
7207 assert_eq!(marks(&score)[1].as_deref(), Some("C"));
7208
7209 let sequence = RehearsalSequence::detect("Z", 1).expect("letters");
7210 assert_eq!(sequence.nth(1, 1), "AA");
7211 assert_eq!(sequence.nth(2, 1), "AB");
7212 let lower = RehearsalSequence::detect("y", 1).expect("lower letters");
7213 assert_eq!(lower.nth(2, 1), "aa");
7214 assert_eq!(
7215 RehearsalSequence::detect("7", 3)
7216 .expect("numbers")
7217 .nth(2, 9),
7218 "9"
7219 );
7220 assert_eq!(
7221 RehearsalSequence::detect("5", 5)
7222 .expect("measure numbers")
7223 .nth(3, 17),
7224 "17"
7225 );
7226 assert!(RehearsalSequence::detect("Intro", 1).is_none());
7227
7228 for_each_measure_at(&mut score, 0, |measure| {
7229 measure.rehearsal = Some("Intro".into())
7230 });
7231 assert!(matches!(
7232 apply_command(&command, &mut score),
7233 Err(Error::InvalidCommand(_))
7234 ));
7235 }
7236
7237 #[test]
7238 fn system_break_interval_replaces_line_breaks_only() {
7239 let mut score = Score::new("Breaks", 120, 4, 4, 0, 10);
7240 score.parts[0].staves[0].measures[1].system_break = true;
7241 score.parts[0].staves[0].measures[2].page_break = true;
7242 apply_command(
7243 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7244 interval: 4,
7245 start_measure: None,
7246 end_measure: None,
7247 }),
7248 &mut score,
7249 )
7250 .expect("breaks every four measures");
7251 let breaks: Vec<usize> = score.parts[0].staves[0]
7252 .measures
7253 .iter()
7254 .enumerate()
7255 .filter(|(_, measure)| measure.system_break)
7256 .map(|(index, _)| index)
7257 .collect();
7258 assert_eq!(breaks, vec![3, 7]);
7259 assert!(score.parts[0].staves[0].measures[2].page_break);
7260
7261 let mut exact = Score::new("Exact", 120, 4, 4, 0, 8);
7262 apply_command(
7263 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7264 interval: 4,
7265 start_measure: None,
7266 end_measure: None,
7267 }),
7268 &mut exact,
7269 )
7270 .expect("no break after the final measure");
7271 assert!(!exact.parts[0].staves[0].measures[7].system_break);
7272 assert!(exact.parts[0].staves[0].measures[3].system_break);
7273
7274 apply_command(
7275 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7276 interval: 0,
7277 start_measure: Some(0),
7278 end_measure: Some(6),
7279 }),
7280 &mut score,
7281 )
7282 .expect("breaks removed in range");
7283 assert!(!score.parts[0].staves[0].measures[3].system_break);
7284 assert!(score.parts[0].staves[0].measures[7].system_break);
7285 assert!(matches!(
7286 apply_command(
7287 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7288 interval: 2,
7289 start_measure: Some(4),
7290 end_measure: Some(11),
7291 }),
7292 &mut score,
7293 ),
7294 Err(Error::InvalidCommand(_))
7295 ));
7296 }
7297
7298 #[test]
7299 fn remove_trailing_empty_measures_keeps_content_and_final_barline() {
7300 let mut score = Score::new("Trailing", 120, 4, 4, 0, 6);
7301 let mut stack = CommandStack::new(50);
7302 score.parts[0].staves[0].measures[1].voices[0] =
7303 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
7304 score.parts[0].staves[0].measures[5].barline_right = Barline::Final;
7305 let mut fermata_rest = Note::rest(Duration::Whole);
7306 fermata_rest.articulations = vec![Articulation::Fermata];
7307 score.parts[0].staves[0].measures[3].voices[0] = vec![fermata_rest];
7308 stack
7309 .execute(
7310 Command::RemoveTrailingEmptyMeasures(RemoveTrailingEmptyMeasuresCmd {}),
7311 &mut score,
7312 )
7313 .expect("trailing measures removed");
7314 let staff = &score.parts[0].staves[0];
7315 assert_eq!(staff.measures.len(), 4, "a rest with a fermata is content");
7316 assert!(matches!(staff.measures[3].barline_right, Barline::Final));
7317 assert_eq!(staff.measures[3].number, 4);
7318 stack.undo(&mut score).expect("removal undoes");
7319 assert_eq!(score.parts[0].staves[0].measures.len(), 6);
7320
7321 let mut empty = Score::new("Empty", 120, 4, 4, 0, 3);
7322 apply_command(
7323 &Command::RemoveTrailingEmptyMeasures(RemoveTrailingEmptyMeasuresCmd {}),
7324 &mut empty,
7325 )
7326 .expect("empty score keeps one measure");
7327 assert_eq!(empty.parts[0].staves[0].measures.len(), 1);
7328 }
7329
7330 #[test]
7331 fn typed_spanner_commands_are_atomic_and_undoable() {
7332 use crate::model::score::{NotationSpanner, NotationSpannerKind};
7333
7334 let mut score = default_engine_score();
7335 let address = NoteAddr {
7336 part: 0,
7337 staff: 0,
7338 measure: 0,
7339 voice: 0,
7340 note: 0,
7341 };
7342 let spanner = NotationSpanner {
7343 id: "slur-1".to_string(),
7344 kind: NotationSpannerKind::Slur,
7345 start: address.clone(),
7346 end: address,
7347 number: Some(1),
7348 line_type: Some("dashed".to_string()),
7349 text: None,
7350 placement: Some("above".to_string()),
7351 ottava_size: None,
7352 ottava_type: None,
7353 };
7354 let mut stack = CommandStack::new(8);
7355 stack
7356 .execute(Command::AddSpanner(AddSpannerCmd { spanner }), &mut score)
7357 .unwrap();
7358 assert_eq!(score.spanners.len(), 1);
7359 assert_eq!(stack.undo_key().as_deref(), Some("AddSpanner"));
7360
7361 let before_duplicate = score.clone();
7362 let duplicate = score.spanners[0].clone();
7363 assert!(
7364 stack
7365 .execute(
7366 Command::AddSpanner(AddSpannerCmd { spanner: duplicate }),
7367 &mut score,
7368 )
7369 .is_err()
7370 );
7371 assert_eq!(score.spanners, before_duplicate.spanners);
7372
7373 let mut updated = score.spanners[0].clone();
7374 updated.number = Some(2);
7375 stack
7376 .execute(
7377 Command::UpdateSpanner(UpdateSpannerCmd { spanner: updated }),
7378 &mut score,
7379 )
7380 .unwrap();
7381 assert_eq!(score.spanners[0].number, Some(2));
7382 stack.undo(&mut score).unwrap();
7383 assert_eq!(score.spanners[0].number, Some(1));
7384 stack.redo(&mut score).unwrap();
7385 assert_eq!(score.spanners[0].number, Some(2));
7386
7387 stack
7388 .execute(
7389 Command::RemoveSpanner(RemoveSpannerCmd {
7390 id: "slur-1".to_string(),
7391 }),
7392 &mut score,
7393 )
7394 .unwrap();
7395 assert!(score.spanners.is_empty());
7396 stack.undo(&mut score).unwrap();
7397 assert_eq!(score.spanners.len(), 1);
7398 }
7399
7400 #[test]
7401 fn string_quartet_linked_views_roundtrip_and_undo_redo_as_one_contract() {
7402 let mut score = Score::template(ScoreTemplate::StringQuartet);
7403 let mut stack = CommandStack::new(16);
7404 for (part, id, name) in [
7405 (0, "violin-1", "Violin I"),
7406 (1, "violin-2", "Violin II"),
7407 (2, "viola", "Viola"),
7408 (3, "cello", "Cello"),
7409 ] {
7410 stack
7411 .execute(
7412 Command::UpsertScoreView(UpsertScoreViewCmd {
7413 view: ScoreView::linked_part(id, name, part),
7414 }),
7415 &mut score,
7416 )
7417 .expect("linked part view applies");
7418 }
7419 assert_eq!(score.parts.len(), 4);
7420 assert_eq!(score.views.len(), 4);
7421 for (part, view) in score.views.iter().enumerate() {
7422 assert_eq!(view.parts, vec![part]);
7423 assert_eq!(score.resolve_view(&view.id).unwrap().parts.len(), 1);
7424 }
7425
7426 let restored: Score =
7427 serde_json::from_str(&serde_json::to_string(&score).expect("quartet score serializes"))
7428 .expect("quartet score deserializes");
7429 assert_eq!(restored.views, score.views);
7430
7431 for _ in 0..4 {
7432 stack.undo(&mut score).expect("view undo applies");
7433 }
7434 assert!(score.views.is_empty());
7435 for _ in 0..4 {
7436 stack.redo(&mut score).expect("view redo applies");
7437 }
7438 assert_eq!(score.views, restored.views);
7439 }
7440
7441 #[test]
7442 fn section_break_is_atomic_undoable_and_synced_across_staves() {
7443 let mut score = Score::template(ScoreTemplate::Piano);
7444 let original = score.clone();
7445 let mut stack = CommandStack::new(4);
7446 stack
7447 .execute(
7448 Command::SetSectionBreak(SetSectionBreakCmd {
7449 measure_index: 2,
7450 value: true,
7451 }),
7452 &mut score,
7453 )
7454 .expect("section break applies");
7455 assert!(
7456 score
7457 .parts
7458 .iter()
7459 .flat_map(|part| part.staves.iter())
7460 .all(|staff| staff.measures[2].section_break)
7461 );
7462 stack.undo(&mut score).expect("section break undoes");
7463 assert_eq!(
7464 serde_json::to_value(&score).unwrap(),
7465 serde_json::to_value(&original).unwrap()
7466 );
7467
7468 assert!(
7469 stack
7470 .execute(
7471 Command::SetSectionBreak(SetSectionBreakCmd {
7472 measure_index: 99,
7473 value: true,
7474 }),
7475 &mut score,
7476 )
7477 .is_err()
7478 );
7479 assert_eq!(
7480 serde_json::to_value(&score).unwrap(),
7481 serde_json::to_value(&original).unwrap()
7482 );
7483 }
7484}