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