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