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