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