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_index in cmd.target_staves.len()..4 {
4215 if !source_measure.voices[voice_index].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_index in 0..cmd.target_staves.len() {
4222 if source_measure.voices[voice_index]
4223 .iter()
4224 .any(|note| note.cross_staff.is_some())
4225 {
4226 return Err(Error::InvalidCommand(
4227 "explode does not support cross-staff source notes".into(),
4228 ));
4229 }
4230 let total: f64 = source_measure.voices[voice_index]
4231 .iter()
4232 .map(Note::beats)
4233 .sum();
4234 if total > expected + 1e-9 {
4235 return Err(Error::InvalidCommand(
4236 "explode source voice exceeds its measure duration".into(),
4237 ));
4238 }
4239 }
4240 for &staff_index in cmd.target_staves.iter().skip(1) {
4241 let target = &part.staves[staff_index];
4242 let target_beats = effective_staff_measure_beats(
4243 target,
4244 &score.settings.time_signature,
4245 measure_index,
4246 )?;
4247 if (target_beats - expected).abs() > 1e-9 {
4248 return Err(Error::InvalidCommand(
4249 "explode target staves must have matching measure durations".into(),
4250 ));
4251 }
4252 let measure = target
4253 .measures
4254 .get(measure_index)
4255 .ok_or(Error::MeasureNotFound(measure_index))?;
4256 if measure.voices[0].iter().any(|note| !note.is_rest)
4257 || measure.voices[1..].iter().any(|voice| !voice.is_empty())
4258 || measure.source_voice_numbers.iter().any(Option::is_some)
4259 {
4260 return Err(Error::InvalidCommand(
4261 "explode destination staff must contain only an unnumbered rest voice".into(),
4262 ));
4263 }
4264 }
4265 }
4266
4267 for measure_index in start..=end {
4268 let transferred: Vec<(Vec<Note>, Option<u32>)> = (0..cmd.target_staves.len())
4269 .map(|voice_index| {
4270 let measure =
4271 &score.parts[cmd.part_index].staves[cmd.source_staff].measures[measure_index];
4272 (
4273 measure.voices[voice_index].clone(),
4274 measure.source_voice_numbers[voice_index],
4275 )
4276 })
4277 .collect();
4278 for (voice_index, &staff_index) in cmd.target_staves.iter().enumerate() {
4279 let (notes, source_voice_number) = &transferred[voice_index];
4280 let target =
4281 &mut score.parts[cmd.part_index].staves[staff_index].measures[measure_index];
4282 target.voices = [notes.clone(), Vec::new(), Vec::new(), Vec::new()];
4283 target.source_voice_numbers = [*source_voice_number, None, None, None];
4284 }
4285 }
4286 remap_spanners(score, |address| {
4287 if address.part != cmd.part_index
4288 || address.staff != cmd.source_staff
4289 || address.measure < start
4290 || address.measure > end
4291 || address.voice >= cmd.target_staves.len()
4292 {
4293 return Some(address.clone());
4294 }
4295 Some(NoteAddr {
4296 staff: cmd.target_staves[address.voice],
4297 voice: 0,
4298 ..address.clone()
4299 })
4300 });
4301 Ok(())
4302}
4303
4304fn clear_derived_chord_note_notation(note: &mut Note) {
4305 note.articulations.clear();
4306 note.dynamic = None;
4307 note.stem_up = None;
4308 note.hairpin_start = None;
4309 note.hairpin_above = None;
4310 note.hairpin_end = false;
4311 note.chord_symbol = None;
4312 note.ottava_start = None;
4313 note.ottava_end = false;
4314 note.lyric = None;
4315 note.additional_lyrics.clear();
4316 note.pedal_start = false;
4317 note.pedal_end = false;
4318 note.slur_start = false;
4319 note.slur_end = false;
4320 note.arpeggiate = None;
4321 note.technique_text = None;
4322 note.glissando_start = false;
4323 note.glissando_end = false;
4324 note.cross_staff = None;
4325 note.fingering = None;
4326 note.fingerings.clear();
4327 note.string_number = None;
4328 note.trill_line_start = false;
4329 note.trill_line_end = false;
4330 note.guitar_technique = None;
4331 note.guitar_bend_alter_cents = None;
4332 note.guitar_bend_curve.clear();
4333}
4334
4335fn exploded_chord_note(source: &Note, pitch_index: usize) -> Note {
4336 if pitch_index == 0 {
4337 let mut retained = source.clone();
4338 if !retained.is_rest {
4339 retained.pitches = vec![source.pitches[0].clone()];
4340 retained.tab_positions = source.tab_positions.first().cloned().into_iter().collect();
4341 retained.tab_position = retained.tab_positions.first().cloned();
4342 }
4343 return retained;
4344 }
4345 let mut derived = source.clone();
4346 derived.id = Uuid::new_v4().to_string();
4347 clear_derived_chord_note_notation(&mut derived);
4348 if derived.is_rest || pitch_index >= source.pitches.len() {
4349 derived.is_rest = true;
4350 derived.is_unpitched = false;
4351 derived.pitches.clear();
4352 derived.tab_position = None;
4353 derived.tab_positions.clear();
4354 } else {
4355 derived.pitches = vec![source.pitches[pitch_index].clone()];
4356 derived.tab_positions = source
4357 .tab_positions
4358 .get(pitch_index)
4359 .cloned()
4360 .into_iter()
4361 .collect();
4362 derived.tab_position = derived.tab_positions.first().cloned();
4363 }
4364 derived
4365}
4366
4367fn apply_explode_chord_pitches(
4368 cmd: &ExplodeChordPitchesCmd,
4369 score: &mut Score,
4370) -> Result<(), Error> {
4371 if !(2..=4).contains(&cmd.target_staves.len()) {
4372 return Err(Error::InvalidCommand(
4373 "chord explode requires two to four target staves".into(),
4374 ));
4375 }
4376 if cmd.target_staves.first() != Some(&cmd.source_staff) {
4377 return Err(Error::InvalidCommand(
4378 "chord explode target staves must begin with the source staff".into(),
4379 ));
4380 }
4381 let mut unique_staves = BTreeSet::new();
4382 for &staff_index in &cmd.target_staves {
4383 if !unique_staves.insert(staff_index) {
4384 return Err(Error::InvalidCommand(
4385 "chord explode target staves must be distinct".into(),
4386 ));
4387 }
4388 }
4389 let (start, end) = normalized_structural_measure_range(cmd.start_measure, cmd.end_measure);
4390 let part = score
4391 .parts
4392 .get(cmd.part_index)
4393 .ok_or(Error::PartNotFound(cmd.part_index))?;
4394 let source = part
4395 .staves
4396 .get(cmd.source_staff)
4397 .ok_or(Error::StaffNotFound(cmd.source_staff))?;
4398 for &staff_index in &cmd.target_staves {
4399 if part.staves.get(staff_index).is_none() {
4400 return Err(Error::StaffNotFound(staff_index));
4401 }
4402 }
4403 for measure_index in start..=end {
4404 let expected =
4405 effective_staff_measure_beats(source, &score.settings.time_signature, measure_index)?;
4406 let source_measure = source
4407 .measures
4408 .get(measure_index)
4409 .ok_or(Error::MeasureNotFound(measure_index))?;
4410 for note in &source_measure.voices[0] {
4411 if note.tuplet.is_some() {
4412 return Err(Error::InvalidCommand(
4413 "chord explode does not support tuplets".into(),
4414 ));
4415 }
4416 if note.cross_staff.is_some() {
4417 return Err(Error::InvalidCommand(
4418 "chord explode does not support cross-staff source notes".into(),
4419 ));
4420 }
4421 if note.is_unpitched || note.is_grace || note.is_cue {
4422 return Err(Error::InvalidCommand(
4423 "chord explode does not support unpitched, grace, or cue notes".into(),
4424 ));
4425 }
4426 if !note.is_rest
4427 && (note.pitches.is_empty() || note.pitches.len() > cmd.target_staves.len())
4428 {
4429 return Err(Error::InvalidCommand(
4430 "chord explode pitch count must fit target staves".into(),
4431 ));
4432 }
4433 }
4434 let total: f64 = source_measure.voices[0].iter().map(Note::beats).sum();
4435 if total > expected + 1e-9 {
4436 return Err(Error::InvalidCommand(
4437 "chord explode source voice exceeds its measure duration".into(),
4438 ));
4439 }
4440 for &staff_index in cmd.target_staves.iter().skip(1) {
4441 let target = &part.staves[staff_index];
4442 let target_beats = effective_staff_measure_beats(
4443 target,
4444 &score.settings.time_signature,
4445 measure_index,
4446 )?;
4447 if (target_beats - expected).abs() > 1e-9 {
4448 return Err(Error::InvalidCommand(
4449 "chord explode target staves must have matching measure durations".into(),
4450 ));
4451 }
4452 let measure = target
4453 .measures
4454 .get(measure_index)
4455 .ok_or(Error::MeasureNotFound(measure_index))?;
4456 if measure.voices[0].iter().any(|note| !note.is_rest)
4457 || measure.voices[1..].iter().any(|voice| !voice.is_empty())
4458 || measure.source_voice_numbers.iter().any(Option::is_some)
4459 {
4460 return Err(Error::InvalidCommand(
4461 "chord explode destination staff must contain only an unnumbered rest voice"
4462 .into(),
4463 ));
4464 }
4465 }
4466 }
4467 for measure_index in start..=end {
4468 let source_measure =
4469 &score.parts[cmd.part_index].staves[cmd.source_staff].measures[measure_index];
4470 let source_voice_number = source_measure.source_voice_numbers[0];
4471 let mut exploded = vec![Vec::new(); cmd.target_staves.len()];
4472 for note in &source_measure.voices[0] {
4473 for (pitch_index, voice) in exploded.iter_mut().enumerate() {
4474 voice.push(exploded_chord_note(note, pitch_index));
4475 }
4476 }
4477 for (index, &staff_index) in cmd.target_staves.iter().enumerate() {
4478 let target =
4479 &mut score.parts[cmd.part_index].staves[staff_index].measures[measure_index];
4480 target.voices = [exploded[index].clone(), Vec::new(), Vec::new(), Vec::new()];
4481 target.source_voice_numbers = [source_voice_number, None, None, None];
4482 }
4483 }
4484 Ok(())
4485}
4486
4487fn scaled_duration(duration: &Duration, scale: DurationScale) -> Option<Duration> {
4488 const ORDER: [Duration; 10] = [
4489 Duration::Longa,
4490 Duration::Breve,
4491 Duration::Whole,
4492 Duration::Half,
4493 Duration::Quarter,
4494 Duration::Eighth,
4495 Duration::Sixteenth,
4496 Duration::ThirtySecond,
4497 Duration::SixtyFourth,
4498 Duration::HundredTwentyEighth,
4499 ];
4500 let index = ORDER.iter().position(|value| value == duration)?;
4501 let scaled = match scale {
4502 DurationScale::Double => index.checked_sub(1)?,
4503 DurationScale::Half => index + 1,
4504 };
4505 ORDER.get(scaled).cloned()
4506}
4507
4508fn uniform_tuplet_ratio(voice: &[Note]) -> Result<Option<TupletInfo>, Error> {
4509 let mut ratio: Option<TupletInfo> = None;
4510 for note in voice {
4511 let Some(tuplet) = ¬e.tuplet else {
4512 continue;
4513 };
4514 if tuplet.actual_notes == 0 || tuplet.normal_notes == 0 {
4515 return Err(Error::InvalidCommand(
4516 "duration scaling requires a non-zero tuplet ratio".into(),
4517 ));
4518 }
4519 if let Some(existing) = &ratio {
4520 if existing != tuplet {
4521 return Err(Error::InvalidCommand(
4522 "duration scaling requires one shared tuplet ratio per voice".into(),
4523 ));
4524 }
4525 } else {
4526 ratio = Some(tuplet.clone());
4527 }
4528 }
4529 Ok(ratio)
4530}
4531
4532fn pad_voice_to_measure_with_tuplet_ratio(
4533 voice: &mut Vec<Note>,
4534 max_beats: f64,
4535 ratio: &TupletInfo,
4536) -> Result<(), Error> {
4537 let mut used: f64 = voice.iter().map(Note::beats).sum();
4538 let ratio_scale = f64::from(ratio.normal_notes) / f64::from(ratio.actual_notes);
4539 while max_beats - used > 1e-9 {
4540 let remaining = max_beats - used;
4541 let duration = [
4542 Duration::Whole,
4543 Duration::Half,
4544 Duration::Quarter,
4545 Duration::Eighth,
4546 Duration::Sixteenth,
4547 Duration::ThirtySecond,
4548 Duration::SixtyFourth,
4549 Duration::HundredTwentyEighth,
4550 ]
4551 .into_iter()
4552 .find(|duration| duration.beats(0) * ratio_scale <= remaining + 1e-9)
4553 .ok_or_else(|| {
4554 Error::InvalidCommand(
4555 "duration scaling cannot represent the remaining tuplet duration".into(),
4556 )
4557 })?;
4558 let mut rest = Note::rest(duration);
4559 rest.tuplet = Some(ratio.clone());
4560 used += rest.beats();
4561 voice.push(rest);
4562 }
4563 Ok(())
4564}
4565
4566fn apply_scale_voice_range(cmd: &ScaleVoiceRangeCmd, score: &mut Score) -> Result<(), Error> {
4567 if cmd.voice >= 4 {
4568 return Err(Error::VoiceOutOfRange(cmd.voice));
4569 }
4570 let (start, end) = normalized_structural_measure_range(cmd.start_measure, cmd.end_measure);
4571 let staff = score
4572 .parts
4573 .get(cmd.part_index)
4574 .ok_or(Error::PartNotFound(cmd.part_index))?
4575 .staves
4576 .get(cmd.staff_index)
4577 .ok_or(Error::StaffNotFound(cmd.staff_index))?;
4578 for measure_index in start..=end {
4579 let measure = staff
4580 .measures
4581 .get(measure_index)
4582 .ok_or(Error::MeasureNotFound(measure_index))?;
4583 let expected =
4584 effective_staff_measure_beats(staff, &score.settings.time_signature, measure_index)?;
4585 let voice = &measure.voices[cmd.voice];
4586 match cmd.tuplet_policy {
4587 TupletScalePolicy::PreserveRatio => {
4588 uniform_tuplet_ratio(voice)?;
4589 }
4590 }
4591 let mut scaled_beats = 0.0;
4592 for note in voice {
4593 if scaled_duration(¬e.duration, cmd.scale).is_none() {
4594 return Err(Error::InvalidCommand(
4595 "duration scaling exceeds the portable duration range".into(),
4596 ));
4597 }
4598 if !note.is_grace && !note.is_cue {
4599 scaled_beats += match cmd.scale {
4600 DurationScale::Half => note.beats() / 2.0,
4601 DurationScale::Double => note.beats() * 2.0,
4602 };
4603 }
4604 }
4605 if scaled_beats > expected + 1e-9 {
4606 return Err(Error::InvalidCommand(format!(
4607 "duration scaling overflows measure {measure_index}: {scaled_beats} beats exceeds {expected}"
4608 )));
4609 }
4610 }
4611
4612 for measure_index in start..=end {
4613 let expected = effective_staff_measure_beats(
4614 &score.parts[cmd.part_index].staves[cmd.staff_index],
4615 &score.settings.time_signature,
4616 measure_index,
4617 )?;
4618 let voice = &mut score.parts[cmd.part_index].staves[cmd.staff_index].measures
4619 [measure_index]
4620 .voices[cmd.voice];
4621 if voice.is_empty() {
4622 continue;
4623 }
4624 let tuplet_ratio = match cmd.tuplet_policy {
4625 TupletScalePolicy::PreserveRatio => uniform_tuplet_ratio(voice)?,
4626 };
4627 for note in voice.iter_mut() {
4628 note.duration = scaled_duration(¬e.duration, cmd.scale).ok_or_else(|| {
4629 Error::InvalidCommand("duration scaling exceeds the portable duration range".into())
4630 })?;
4631 }
4632 if let Some(ratio) = tuplet_ratio {
4633 pad_voice_to_measure_with_tuplet_ratio(voice, expected, &ratio)?;
4634 } else {
4635 pad_voice_to_measure(voice, expected);
4636 }
4637 }
4638 Ok(())
4639}
4640
4641fn apply_paste_score_fragment(cmd: &PasteScoreFragmentCmd, score: &mut Score) -> Result<(), Error> {
4642 if !(MIN_SUPPORTED_SCORE_FRAGMENT_CONTRACT_VERSION..=SCORE_FRAGMENT_CONTRACT_VERSION)
4643 .contains(&cmd.fragment.contract_version)
4644 {
4645 return Err(Error::InvalidCommand(format!(
4646 "unsupported score fragment contract version {}",
4647 cmd.fragment.contract_version
4648 )));
4649 }
4650 if cmd.fragment.voices.is_empty() {
4651 return Err(Error::InvalidCommand(
4652 "cannot paste an empty score fragment".into(),
4653 ));
4654 }
4655
4656 let mut lanes = BTreeSet::new();
4660 let mut replaced = BTreeSet::new();
4661 for lane in &cmd.fragment.voices {
4662 let part_index = cmd
4663 .target
4664 .part
4665 .checked_add(lane.relative_part)
4666 .ok_or_else(|| Error::InvalidCommand("fragment part target overflows".into()))?;
4667 let staff_index = cmd
4668 .target
4669 .staff
4670 .checked_add(lane.relative_staff)
4671 .ok_or_else(|| Error::InvalidCommand("fragment staff target overflows".into()))?;
4672 let voice_index = cmd
4673 .target
4674 .voice
4675 .checked_add(lane.relative_voice)
4676 .ok_or_else(|| Error::InvalidCommand("fragment voice target overflows".into()))?;
4677 if voice_index >= 4 {
4678 return Err(Error::VoiceOutOfRange(voice_index));
4679 }
4680 let staff = score
4681 .parts
4682 .get(part_index)
4683 .ok_or(Error::PartNotFound(part_index))?
4684 .staves
4685 .get(staff_index)
4686 .ok_or(Error::StaffNotFound(staff_index))?;
4687 if !lanes.insert((part_index, staff_index, voice_index)) {
4688 return Err(Error::InvalidCommand(
4689 "fragment contains duplicate destination voice lanes".into(),
4690 ));
4691 }
4692 for measure in &lane.measures {
4693 let measure_index = cmd
4694 .target
4695 .measure
4696 .checked_add(measure.relative_measure)
4697 .ok_or_else(|| Error::InvalidCommand("fragment measure target overflows".into()))?;
4698 if measure_index >= staff.measures.len() {
4699 return Err(Error::MeasureNotFound(measure_index));
4700 }
4701 if !measure.cross_staff_targets.is_empty()
4702 && measure.cross_staff_targets.len() != measure.notes.len()
4703 {
4704 return Err(Error::InvalidCommand(
4705 "fragment cross-staff targets do not match note count".into(),
4706 ));
4707 }
4708 if cmd.policy == ScoreFragmentPastePolicy::Merge
4709 && !measure.notes.is_empty()
4710 && staff.measures[measure_index].voices[voice_index]
4711 .iter()
4712 .any(|note| !note.is_rest)
4713 {
4714 return Err(Error::InvalidCommand(
4715 "fragment merge would overwrite a sounding destination lane".into(),
4716 ));
4717 }
4718 if cmd.policy == ScoreFragmentPastePolicy::Merge
4719 && measure.attributes.present
4720 && measure.attributes
4721 != super::fragment::ScoreFragmentMeasureAttributes::from_measure(
4722 &staff.measures[measure_index],
4723 )
4724 {
4725 return Err(Error::InvalidCommand(
4726 "fragment merge would overwrite destination measure attributes".into(),
4727 ));
4728 }
4729 for cross_staff in measure.cross_staff_targets.iter().flatten() {
4730 let target_staff = staff_index as i64 + cross_staff.staff_offset;
4731 if target_staff < 0
4732 || score.parts[part_index]
4733 .staves
4734 .get(target_staff as usize)
4735 .is_none()
4736 {
4737 return Err(Error::InvalidCommand(
4738 "fragment cross-staff target is outside destination part".into(),
4739 ));
4740 }
4741 if cross_staff.target_voice.is_some_and(|voice| voice >= 4) {
4742 return Err(Error::InvalidCommand(
4743 "fragment cross-staff target voice is outside editable range".into(),
4744 ));
4745 }
4746 }
4747 replaced.insert((part_index, staff_index, measure_index, voice_index));
4748 }
4749 }
4750
4751 score.spanners.retain(|spanner| {
4755 !replaced.contains(&(
4756 spanner.start.part,
4757 spanner.start.staff,
4758 spanner.start.measure,
4759 spanner.start.voice,
4760 )) && !replaced.contains(&(
4761 spanner.end.part,
4762 spanner.end.staff,
4763 spanner.end.measure,
4764 spanner.end.voice,
4765 ))
4766 });
4767 for lane in &cmd.fragment.voices {
4768 let part_index = cmd.target.part + lane.relative_part;
4769 let staff_index = cmd.target.staff + lane.relative_staff;
4770 let voice_index = cmd.target.voice + lane.relative_voice;
4771 let staff = &mut score.parts[part_index].staves[staff_index];
4772 for measure in &lane.measures {
4773 let measure_index = cmd.target.measure + measure.relative_measure;
4774 let target = &mut staff.measures[measure_index];
4775 if cmd.policy == ScoreFragmentPastePolicy::Merge && measure.notes.is_empty() {
4776 continue;
4777 }
4778 let mut notes = measure.notes.clone();
4779 for note in &mut notes {
4780 note.id = Uuid::new_v4().to_string();
4781 }
4782 if !measure.cross_staff_targets.is_empty() {
4783 for (note, cross_staff) in notes.iter_mut().zip(&measure.cross_staff_targets) {
4784 note.cross_staff = cross_staff.as_ref().map(|cross_staff| CrossStaff {
4785 target_staff: (staff_index as i64 + cross_staff.staff_offset) as usize,
4786 target_voice: cross_staff.target_voice,
4787 });
4788 }
4789 }
4790 target.voices[voice_index] = notes;
4791 target.source_voice_numbers[voice_index] = measure.source_voice_number;
4792 if cmd.policy == ScoreFragmentPastePolicy::Replace {
4793 measure.attributes.apply_to_measure(target);
4794 }
4795 }
4796 }
4797
4798 let mut used_spanner_ids = score
4799 .spanners
4800 .iter()
4801 .map(|spanner| spanner.id.clone())
4802 .collect::<BTreeSet<_>>();
4803 for source in &cmd.fragment.spanners {
4804 let mut copied = source.clone();
4805 copied.start = fragment_destination_address(&cmd.target, &source.start)?;
4806 copied.end = fragment_destination_address(&cmd.target, &source.end)?;
4807 if note_at(score, &copied.start).is_none() {
4808 return Err(Error::NoteNotFound(copied.start.note));
4809 }
4810 if note_at(score, &copied.end).is_none() {
4811 return Err(Error::NoteNotFound(copied.end.note));
4812 }
4813 copied.id = unique_fragment_spanner_id(&used_spanner_ids, &source.id);
4814 used_spanner_ids.insert(copied.id.clone());
4815 score.spanners.push(copied);
4816 }
4817 Ok(())
4818}
4819
4820fn fragment_destination_address(target: &NoteAddr, relative: &NoteAddr) -> Result<NoteAddr, Error> {
4821 Ok(NoteAddr {
4822 part: target
4823 .part
4824 .checked_add(relative.part)
4825 .ok_or_else(|| Error::InvalidCommand("fragment part target overflows".into()))?,
4826 staff: target
4827 .staff
4828 .checked_add(relative.staff)
4829 .ok_or_else(|| Error::InvalidCommand("fragment staff target overflows".into()))?,
4830 measure: target
4831 .measure
4832 .checked_add(relative.measure)
4833 .ok_or_else(|| Error::InvalidCommand("fragment measure target overflows".into()))?,
4834 voice: target
4835 .voice
4836 .checked_add(relative.voice)
4837 .ok_or_else(|| Error::InvalidCommand("fragment voice target overflows".into()))?,
4838 note: relative.note,
4839 })
4840}
4841
4842fn unique_fragment_spanner_id(used: &BTreeSet<String>, source_id: &str) -> String {
4843 let base = format!("{source_id}-copy");
4844 if !used.contains(&base) {
4845 return base;
4846 }
4847 let mut suffix = 2usize;
4848 loop {
4849 let candidate = format!("{base}-{suffix}");
4850 if !used.contains(&candidate) {
4851 return candidate;
4852 }
4853 suffix += 1;
4854 }
4855}
4856
4857fn apply_toggle_slur(cmd: &ToggleSlurCmd, score: &mut Score) -> Result<(), Error> {
4858 let new_start = !{
4859 score
4860 .parts
4861 .get(cmd.start.part)
4862 .ok_or(Error::PartNotFound(cmd.start.part))?
4863 .staves
4864 .get(cmd.start.staff)
4865 .ok_or(Error::StaffNotFound(cmd.start.staff))?
4866 .measures
4867 .get(cmd.start.measure)
4868 .ok_or(Error::MeasureNotFound(cmd.start.measure))?
4869 .voices
4870 .get(cmd.start.voice)
4871 .ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
4872 .get(cmd.start.note)
4873 .ok_or(Error::NoteNotFound(cmd.start.note))?
4874 .slur_start
4875 };
4876 let new_end = !{
4877 score
4878 .parts
4879 .get(cmd.end.part)
4880 .ok_or(Error::PartNotFound(cmd.end.part))?
4881 .staves
4882 .get(cmd.end.staff)
4883 .ok_or(Error::StaffNotFound(cmd.end.staff))?
4884 .measures
4885 .get(cmd.end.measure)
4886 .ok_or(Error::MeasureNotFound(cmd.end.measure))?
4887 .voices
4888 .get(cmd.end.voice)
4889 .ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
4890 .get(cmd.end.note)
4891 .ok_or(Error::NoteNotFound(cmd.end.note))?
4892 .slur_end
4893 };
4894 score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
4895 [cmd.start.voice][cmd.start.note]
4896 .slur_start = new_start;
4897 score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
4898 [cmd.end.voice][cmd.end.note]
4899 .slur_end = new_end;
4900 Ok(())
4901}
4902
4903fn apply_toggle_trill_line(cmd: &ToggleTrillLineCmd, score: &mut Score) -> Result<(), Error> {
4904 let new_start = !{
4905 score
4906 .parts
4907 .get(cmd.start.part)
4908 .ok_or(Error::PartNotFound(cmd.start.part))?
4909 .staves
4910 .get(cmd.start.staff)
4911 .ok_or(Error::StaffNotFound(cmd.start.staff))?
4912 .measures
4913 .get(cmd.start.measure)
4914 .ok_or(Error::MeasureNotFound(cmd.start.measure))?
4915 .voices
4916 .get(cmd.start.voice)
4917 .ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
4918 .get(cmd.start.note)
4919 .ok_or(Error::NoteNotFound(cmd.start.note))?
4920 .trill_line_start
4921 };
4922 let new_end = !{
4923 score
4924 .parts
4925 .get(cmd.end.part)
4926 .ok_or(Error::PartNotFound(cmd.end.part))?
4927 .staves
4928 .get(cmd.end.staff)
4929 .ok_or(Error::StaffNotFound(cmd.end.staff))?
4930 .measures
4931 .get(cmd.end.measure)
4932 .ok_or(Error::MeasureNotFound(cmd.end.measure))?
4933 .voices
4934 .get(cmd.end.voice)
4935 .ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
4936 .get(cmd.end.note)
4937 .ok_or(Error::NoteNotFound(cmd.end.note))?
4938 .trill_line_end
4939 };
4940 score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
4941 [cmd.start.voice][cmd.start.note]
4942 .trill_line_start = new_start;
4943 score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
4944 [cmd.end.voice][cmd.end.note]
4945 .trill_line_end = new_end;
4946 Ok(())
4947}
4948
4949fn apply_add_staff(cmd: &AddStaffCmd, score: &mut Score) -> Result<(), Error> {
4950 let ts = score.settings.time_signature.clone();
4951 let measure_count = score
4952 .parts
4953 .get(cmd.part_index)
4954 .ok_or(Error::PartNotFound(cmd.part_index))?
4955 .staves
4956 .first()
4957 .map_or(0, |s| s.measures.len());
4958 let mut staff = Staff::new(cmd.clef.clone());
4959 for i in 0..measure_count {
4960 let mut m = Measure::empty(ts.numerator, ts.denominator);
4961 m.number = i as u32 + 1;
4962 staff.measures.push(m);
4963 }
4964 score.parts[cmd.part_index].staves.push(staff);
4965 Ok(())
4966}
4967
4968fn apply_delete_staff(cmd: &DeleteStaffCmd, score: &mut Score) -> Result<(), Error> {
4969 let part = score
4970 .parts
4971 .get_mut(cmd.part_index)
4972 .ok_or(Error::PartNotFound(cmd.part_index))?;
4973 if part.staves.len() <= 1 {
4974 return Err(Error::CannotDeleteLastStaff);
4975 }
4976 if cmd.staff_index >= part.staves.len() {
4977 return Err(Error::StaffNotFound(cmd.staff_index));
4978 }
4979 part.staves.remove(cmd.staff_index);
4980 remap_spanners(score, |address| {
4981 if address.part != cmd.part_index {
4982 Some(address.clone())
4983 } else if address.staff == cmd.staff_index {
4984 None
4985 } else if address.staff > cmd.staff_index {
4986 let mut shifted = address.clone();
4987 shifted.staff -= 1;
4988 Some(shifted)
4989 } else {
4990 Some(address.clone())
4991 }
4992 });
4993 Ok(())
4994}
4995
4996fn same_voice(address: &NoteAddr, part: usize, staff: usize, measure: usize, voice: usize) -> bool {
4998 address.part == part
4999 && address.staff == staff
5000 && address.measure == measure
5001 && address.voice == voice
5002}
5003
5004fn trim_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
5005 if matches!(voice.as_slice(), [only] if only.is_plain_whole_rest()) {
5007 return;
5008 }
5009 let mut total = 0.0f64;
5010 let mut cutoff = voice.len();
5011 for (i, n) in voice.iter().enumerate() {
5012 total += n.beats();
5013 if total > max_beats + 1e-9 {
5014 cutoff = i;
5015 break;
5016 }
5017 }
5018 voice.truncate(cutoff);
5019 pad_voice_to_measure(voice, max_beats);
5020}
5021
5022fn pad_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
5023 let mut used = crate::voice_duration_beats(voice, max_beats);
5024 while max_beats - used > 1e-9 {
5025 let remaining = max_beats - used;
5026 let rest = Note::rest(Duration::whole_filling_beats(remaining));
5027 used += rest.beats();
5028 voice.push(rest);
5029 }
5030}
5031
5032#[cfg(test)]
5033mod tests {
5034 use super::*;
5035 use crate::ScoreEngine;
5036 use crate::model::pitch::Step;
5037
5038 fn default_engine_score() -> Score {
5039 let mut s = Score::default();
5040 for part in &mut s.parts {
5041 for staff in &mut part.staves {
5042 for (i, m) in staff.measures.iter_mut().enumerate() {
5043 m.number = i as u32 + 1;
5044 }
5045 }
5046 }
5047 s
5048 }
5049
5050 #[test]
5051 fn legacy_scale_voice_range_command_defaults_to_preserve_tuplet_ratio() {
5052 let command: Command = serde_json::from_str(
5053 r#"{"type":"scale_voice_range","part_index":0,"staff_index":0,"voice":0,"start_measure":0,"end_measure":0,"scale":"half"}"#,
5054 )
5055 .expect("legacy scale command deserializes");
5056 let Command::ScaleVoiceRange(command) = command else {
5057 panic!("expected scale command");
5058 };
5059 assert_eq!(command.tuplet_policy, TupletScalePolicy::PreserveRatio);
5060 }
5061
5062 fn score_with_typed_spanner() -> Score {
5063 use crate::model::score::{NotationSpanner, NotationSpannerKind};
5064
5065 let mut score = Score::new("Spanner edits", 120, 4, 4, 0, 1);
5066 score.parts[0].staves[0].measures[0].voices[0] = (0..4)
5067 .map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
5068 .collect();
5069 score.spanners.push(NotationSpanner {
5070 id: "typed-glissando".to_string(),
5071 kind: NotationSpannerKind::Glissando,
5072 start: NoteAddr {
5073 part: 0,
5074 staff: 0,
5075 measure: 0,
5076 voice: 0,
5077 note: 1,
5078 },
5079 end: NoteAddr {
5080 part: 0,
5081 staff: 0,
5082 measure: 0,
5083 voice: 0,
5084 note: 2,
5085 },
5086 number: Some(1),
5087 line_type: None,
5088 text: None,
5089 placement: None,
5090 ottava_size: None,
5091 ottava_type: None,
5092 });
5093 score
5094 }
5095
5096 #[test]
5097 fn deleting_staff_remaps_spanners_and_undo_restores_them() {
5098 let mut score = score_with_typed_spanner();
5099 let staff = score.parts[0].staves[0].clone();
5100 score.parts[0].staves.extend([staff.clone(), staff]);
5101 score.parts.push(score.parts[0].clone());
5102
5103 let mut shifted = score.spanners[0].clone();
5104 shifted.id = "shifted".into();
5105 shifted.start.staff = 2;
5106 shifted.end.staff = 2;
5107 let mut removed = score.spanners[0].clone();
5108 removed.id = "removed".into();
5109 removed.start.staff = 1;
5110 removed.end.staff = 1;
5111 let mut other_part = score.spanners[0].clone();
5112 other_part.id = "other-part".into();
5113 other_part.start.part = 1;
5114 other_part.end.part = 1;
5115 score.spanners.extend([shifted, removed, other_part]);
5116 let original = score.spanners.clone();
5117
5118 let mut stack = CommandStack::new(8);
5119 stack
5120 .execute(
5121 Command::DeleteStaff(DeleteStaffCmd {
5122 part_index: 0,
5123 staff_index: 1,
5124 }),
5125 &mut score,
5126 )
5127 .unwrap();
5128 assert_eq!(score.parts[0].staves.len(), 2);
5129 assert!(!score.spanners.iter().any(|span| span.id == "removed"));
5130 let shifted = score
5131 .spanners
5132 .iter()
5133 .find(|span| span.id == "shifted")
5134 .unwrap();
5135 assert_eq!((shifted.start.staff, shifted.end.staff), (1, 1));
5136 let other = score
5137 .spanners
5138 .iter()
5139 .find(|span| span.id == "other-part")
5140 .unwrap();
5141 assert_eq!((other.start.part, other.end.part), (1, 1));
5142
5143 stack.undo(&mut score).unwrap();
5144 assert_eq!(score.parts[0].staves.len(), 3);
5145 assert_eq!(score.spanners, original);
5146 stack.redo(&mut score).unwrap();
5147 assert_eq!(score.parts[0].staves.len(), 2);
5148 assert!(!score.spanners.iter().any(|span| span.id == "removed"));
5149 }
5150
5151 fn insert_note_at(position: usize) -> Command {
5152 Command::AddNote(AddNoteCmd {
5153 part_index: 0,
5154 staff_index: 0,
5155 measure_index: 0,
5156 voice: 0,
5157 position,
5158 pitch: Some(Pitch::new(Step::D, 5)),
5159 duration: Duration::Quarter,
5160 dot_count: 0,
5161 is_rest: false,
5162 tuplet: None,
5163 })
5164 }
5165
5166 #[test]
5167 fn add_note_inserts_into_voice() {
5168 let mut score = default_engine_score();
5169 let cmd = Command::AddNote(AddNoteCmd {
5170 part_index: 0,
5171 staff_index: 0,
5172 measure_index: 0,
5173 voice: 0,
5174 position: 0,
5175 pitch: Some(Pitch::new(Step::C, 4)),
5176 duration: Duration::Quarter,
5177 dot_count: 0,
5178 is_rest: false,
5179 tuplet: None,
5180 });
5181 apply_command(&cmd, &mut score).unwrap();
5182 let first = &score.parts[0].staves[0].measures[0].voices[0][0];
5183 assert!(!first.is_rest);
5184 assert_eq!(first.pitches[0].step, Step::C);
5185 }
5186
5187 #[test]
5188 fn typed_spanners_follow_note_insertions_at_every_relative_position() {
5189 for (position, expected_start, expected_end) in [
5190 (0, 2, 3), (1, 2, 3), (2, 1, 3), (4, 1, 2), (3, 1, 2), ] {
5196 let mut score = score_with_typed_spanner();
5197 apply_command(&insert_note_at(position), &mut score).unwrap();
5198 let span = score.spanners.first().expect("span remains attached");
5199 assert_eq!(span.start.note, expected_start, "position {position}");
5200 assert_eq!(span.end.note, expected_end, "position {position}");
5201 }
5202 }
5203
5204 #[test]
5205 fn deleting_a_typed_spanner_endpoint_removes_the_whole_span_and_undo_redo_is_atomic() {
5206 let mut score = score_with_typed_spanner();
5207 let endpoint_id = score.parts[0].staves[0].measures[0].voices[0][1].id.clone();
5208 let mut stack = CommandStack::new(8);
5209
5210 stack
5211 .execute(
5212 Command::DeleteNote(DeleteNoteCmd {
5213 note_id: endpoint_id,
5214 part_index: 0,
5215 staff_index: 0,
5216 measure_index: 0,
5217 voice: 0,
5218 }),
5219 &mut score,
5220 )
5221 .unwrap();
5222 assert!(score.spanners.is_empty());
5223
5224 stack.undo(&mut score).unwrap();
5225 assert_eq!(score.spanners[0].start.note, 1);
5226 assert_eq!(score.spanners[0].end.note, 2);
5227
5228 stack.redo(&mut score).unwrap();
5229 assert!(score.spanners.is_empty());
5230 }
5231
5232 #[test]
5233 fn batched_structural_edits_remap_typed_spanners_and_restore_them_on_undo() {
5234 let mut score = score_with_typed_spanner();
5235 let mut stack = CommandStack::new(8);
5236 stack
5237 .batch_execute(
5238 vec![
5239 insert_note_at(0),
5240 Command::SetTempo(SetTempoCmd { bpm: 144 }),
5241 ],
5242 &mut score,
5243 )
5244 .unwrap();
5245 assert_eq!(score.spanners[0].start.note, 2);
5246 assert_eq!(score.spanners[0].end.note, 3);
5247
5248 stack.undo(&mut score).unwrap();
5249 assert_eq!(score.spanners[0].start.note, 1);
5250 assert_eq!(score.spanners[0].end.note, 2);
5251
5252 stack.redo(&mut score).unwrap();
5253 assert_eq!(score.spanners[0].start.note, 2);
5254 assert_eq!(score.spanners[0].end.note, 3);
5255 }
5256
5257 #[test]
5258 fn typed_spanners_remap_only_the_edited_endpoint_across_staff_and_voice() {
5259 use crate::model::score::{NotationSpanner, NotationSpannerKind, ScoreTemplate};
5260
5261 let mut score = Score::template(ScoreTemplate::Piano);
5262 for staff in &mut score.parts[0].staves {
5263 let notes: Vec<Note> = (0..4)
5264 .map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
5265 .collect();
5266 staff.measures[0].voices[0] = notes.clone();
5267 staff.measures[0].voices[1] = notes;
5268 }
5269 score.spanners.push(NotationSpanner {
5270 id: "cross-staff".to_string(),
5271 kind: NotationSpannerKind::Slur,
5272 start: NoteAddr {
5273 part: 0,
5274 staff: 0,
5275 measure: 0,
5276 voice: 0,
5277 note: 1,
5278 },
5279 end: NoteAddr {
5280 part: 0,
5281 staff: 1,
5282 measure: 0,
5283 voice: 1,
5284 note: 2,
5285 },
5286 number: Some(2),
5287 line_type: None,
5288 text: None,
5289 placement: None,
5290 ottava_size: None,
5291 ottava_type: None,
5292 });
5293
5294 let mut stack = CommandStack::new(8);
5295 stack.execute(insert_note_at(0), &mut score).unwrap();
5296 let span = &score.spanners[0];
5297 assert_eq!(span.start.note, 2);
5298 assert_eq!(span.end.staff, 1);
5299 assert_eq!(span.end.voice, 1);
5300 assert_eq!(span.end.note, 2);
5301 }
5302
5303 #[test]
5304 fn set_fingerings_keeps_first_legacy_value_in_sync() {
5305 let mut score = default_engine_score();
5306 apply_command(
5307 &Command::AddNote(AddNoteCmd {
5308 part_index: 0,
5309 staff_index: 0,
5310 measure_index: 0,
5311 voice: 0,
5312 position: 0,
5313 pitch: Some(Pitch::new(Step::C, 4)),
5314 duration: Duration::Quarter,
5315 dot_count: 0,
5316 is_rest: false,
5317 tuplet: None,
5318 }),
5319 &mut score,
5320 )
5321 .unwrap();
5322 apply_command(
5323 &Command::SetFingerings(SetFingeringsCmd {
5324 part_index: 0,
5325 staff_index: 0,
5326 measure_index: 0,
5327 voice: 0,
5328 note_index: 0,
5329 fingerings: vec![1, 3, 4],
5330 }),
5331 &mut score,
5332 )
5333 .unwrap();
5334 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
5335 assert_eq!(note.fingerings, vec![1, 3, 4]);
5336 assert_eq!(note.fingering, Some(1));
5337 }
5338
5339 #[test]
5340 fn set_figured_bass_replaces_measure_figures() {
5341 let mut score = default_engine_score();
5342 let figures = vec![FiguredBassFigure {
5343 number: "6".to_string(),
5344 alter: Some("-1".to_string()),
5345 prefix: Some("+".to_string()),
5346 suffix: None,
5347 extender: false,
5348 }];
5349 apply_command(
5350 &Command::SetFiguredBass(SetFiguredBassCmd {
5351 measure_index: 0,
5352 figures: figures.clone(),
5353 }),
5354 &mut score,
5355 )
5356 .unwrap();
5357 assert_eq!(score.parts[0].staves[0].measures[0].figured_bass, figures);
5358 }
5359
5360 #[test]
5361 fn set_measure_text_supports_append_replace_remove_and_undo_redo() {
5362 let mut engine = crate::ScoreEngine::new();
5363 let address = SetMeasureTextCmd {
5364 part_index: 0,
5365 staff_index: 0,
5366 measure_index: 0,
5367 text_index: 0,
5368 text: Some(StyledText {
5369 position_beats: None,
5370 style: crate::TextStyle::Technique,
5371 text: "dolce".to_string(),
5372 placement: None,
5373 offset_x: None,
5374 offset_y: None,
5375 relative_x: None,
5376 relative_y: None,
5377 }),
5378 };
5379 engine.apply(Command::SetMeasureText(address)).unwrap();
5380 assert_eq!(
5381 engine.score.parts[0].staves[0].measures[0].texts[0],
5382 StyledText {
5383 position_beats: None,
5384 style: crate::TextStyle::Technique,
5385 text: "dolce".to_string(),
5386 placement: None,
5387 offset_x: None,
5388 offset_y: None,
5389 relative_x: None,
5390 relative_y: None,
5391 }
5392 );
5393
5394 engine
5395 .apply(Command::SetMeasureText(SetMeasureTextCmd {
5396 text_index: 0,
5397 text: Some(StyledText {
5398 position_beats: None,
5399 style: crate::TextStyle::RehearsalMark,
5400 text: "A".to_string(),
5401 placement: None,
5402 offset_x: None,
5403 offset_y: None,
5404 relative_x: None,
5405 relative_y: None,
5406 }),
5407 ..SetMeasureTextCmd {
5408 part_index: 0,
5409 staff_index: 0,
5410 measure_index: 0,
5411 text_index: 0,
5412 text: None,
5413 }
5414 }))
5415 .unwrap();
5416 assert_eq!(
5417 engine.score.parts[0].staves[0].measures[0].texts[0].style,
5418 crate::TextStyle::RehearsalMark
5419 );
5420
5421 engine
5422 .apply(Command::SetMeasureText(SetMeasureTextCmd {
5423 text_index: 0,
5424 text: None,
5425 ..SetMeasureTextCmd {
5426 part_index: 0,
5427 staff_index: 0,
5428 measure_index: 0,
5429 text_index: 0,
5430 text: None,
5431 }
5432 }))
5433 .unwrap();
5434 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
5435 engine.undo().unwrap();
5436 assert_eq!(engine.score.parts[0].staves[0].measures[0].texts.len(), 1);
5437 engine.redo().unwrap();
5438 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
5439 }
5440
5441 #[test]
5442 fn set_measure_text_rejects_invalid_index_atomically_and_round_trips_json() {
5443 let mut score = default_engine_score();
5444 let before = score.clone();
5445 let command = Command::SetMeasureText(SetMeasureTextCmd {
5446 part_index: 0,
5447 staff_index: 0,
5448 measure_index: 0,
5449 text_index: 2,
5450 text: Some(StyledText {
5451 position_beats: None,
5452 style: crate::TextStyle::Expression,
5453 text: "espressivo".to_string(),
5454 placement: None,
5455 offset_x: None,
5456 offset_y: None,
5457 relative_x: None,
5458 relative_y: None,
5459 }),
5460 });
5461 let json = serde_json::to_string(&command).unwrap();
5462 let restored: Command = serde_json::from_str(&json).unwrap();
5463 assert_eq!(
5464 serde_json::to_value(&restored).unwrap(),
5465 serde_json::to_value(&command).unwrap()
5466 );
5467 assert!(apply_command(&restored, &mut score).is_err());
5468 assert_eq!(
5469 serde_json::to_value(&score).unwrap(),
5470 serde_json::to_value(&before).unwrap()
5471 );
5472 }
5473
5474 #[test]
5475 fn set_score_text_supports_append_replace_remove_and_undo_redo() {
5476 let mut engine = crate::ScoreEngine::new();
5477 let text = StyledText {
5478 position_beats: None,
5479 style: crate::TextStyle::Expression,
5480 text: "Title".to_string(),
5481 placement: None,
5482 offset_x: None,
5483 offset_y: None,
5484 relative_x: None,
5485 relative_y: None,
5486 };
5487 engine
5488 .apply(Command::SetScoreText(SetScoreTextCmd {
5489 text_index: 0,
5490 text: Some(text.clone()),
5491 }))
5492 .unwrap();
5493 assert_eq!(engine.score.texts, vec![text.clone()]);
5494
5495 let mut replacement = text.clone();
5496 replacement.text = "Subtitle".to_string();
5497 engine
5498 .apply(Command::SetScoreText(SetScoreTextCmd {
5499 text_index: 0,
5500 text: Some(replacement.clone()),
5501 }))
5502 .unwrap();
5503 assert_eq!(engine.score.texts, vec![replacement]);
5504
5505 engine
5506 .apply(Command::SetScoreText(SetScoreTextCmd {
5507 text_index: 0,
5508 text: None,
5509 }))
5510 .unwrap();
5511 assert!(engine.score.texts.is_empty());
5512 engine.undo().unwrap();
5513 assert_eq!(engine.score.texts.len(), 1);
5514 engine.redo().unwrap();
5515 assert!(engine.score.texts.is_empty());
5516 }
5517
5518 #[test]
5519 fn set_score_style_overrides_is_undoable_and_rejects_invalid_values() {
5520 let mut engine = crate::ScoreEngine::new();
5521 let overrides = vec![ViewStyleOverride {
5522 property: super::super::score::ViewStyleProperty::SystemGap,
5523 value: 2.5,
5524 }];
5525 engine
5526 .apply(Command::SetScoreStyleOverrides(SetScoreStyleOverridesCmd {
5527 overrides: overrides.clone(),
5528 }))
5529 .unwrap();
5530 assert_eq!(engine.score.style_overrides, overrides);
5531 engine.undo().unwrap();
5532 assert!(engine.score.style_overrides.is_empty());
5533 engine.redo().unwrap();
5534 assert_eq!(engine.score.style_overrides, overrides);
5535
5536 let before = engine.score.clone();
5537 assert!(
5538 engine
5539 .apply(Command::SetScoreStyleOverrides(SetScoreStyleOverridesCmd {
5540 overrides: vec![ViewStyleOverride {
5541 property: super::super::score::ViewStyleProperty::TextScale,
5542 value: f32::NAN,
5543 }],
5544 }))
5545 .is_err()
5546 );
5547 assert_eq!(
5548 serde_json::to_value(&engine.score).unwrap(),
5549 serde_json::to_value(&before).unwrap()
5550 );
5551 }
5552
5553 #[test]
5554 fn set_object_style_overrides_is_undoable_and_requires_existing_target() {
5555 let mut engine = crate::ScoreEngine::new();
5556 engine.score.texts.push(StyledText {
5557 position_beats: None,
5558 style: crate::TextStyle::Expression,
5559 text: "Allegro".into(),
5560 placement: None,
5561 offset_x: None,
5562 offset_y: None,
5563 relative_x: None,
5564 relative_y: None,
5565 });
5566 let overrides = vec![ObjectStyleOverride {
5567 target: super::super::score::ObjectStyleTarget::ScoreText { text_index: 0 },
5568 property: super::super::score::ViewStyleProperty::TextScale,
5569 value: 1.2,
5570 provenance: None,
5571 }];
5572 engine
5573 .apply(Command::SetObjectStyleOverrides(
5574 SetObjectStyleOverridesCmd {
5575 overrides: overrides.clone(),
5576 },
5577 ))
5578 .unwrap();
5579 assert_eq!(engine.score.object_style_overrides, overrides);
5580 engine.undo().unwrap();
5581 assert!(engine.score.object_style_overrides.is_empty());
5582
5583 assert!(
5584 engine
5585 .apply(Command::SetObjectStyleOverrides(
5586 SetObjectStyleOverridesCmd {
5587 overrides: vec![ObjectStyleOverride {
5588 target: super::super::score::ObjectStyleTarget::ScoreText {
5589 text_index: 1,
5590 },
5591 property: super::super::score::ViewStyleProperty::TextScale,
5592 value: 1.2,
5593 provenance: None,
5594 }],
5595 }
5596 ))
5597 .is_err()
5598 );
5599 }
5600
5601 #[test]
5602 fn set_harmony_range_is_undoable_and_json_compatible() {
5603 let mut score = default_engine_score();
5604 score.parts[0].staves[0].measures[0].voices[0] =
5605 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
5606 score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
5607 root: "C".to_owned(),
5608 kind: "major".to_owned(),
5609 bass: None,
5610 placement: None,
5611 extender: true,
5612 harmonic_degree: None,
5613 harmony_function: None,
5614 harmony_type: None,
5615 chord_ref: None,
5616 range_end: None,
5617 degrees: Vec::new(),
5618 });
5619 let mut engine = ScoreEngine::new();
5620 engine.replace_score(score);
5621 let command = Command::SetHarmonyRange(SetHarmonyRangeCmd {
5622 part_index: 0,
5623 staff_index: 0,
5624 measure_index: 0,
5625 voice: 0,
5626 note_index: 0,
5627 end: Some(NoteAddr {
5628 part: 0,
5629 staff: 0,
5630 measure: 0,
5631 voice: 0,
5632 note: 0,
5633 }),
5634 });
5635 let json = serde_json::to_string(&command).unwrap();
5636 let restored: Command = serde_json::from_str(&json).unwrap();
5637 engine.apply(restored).unwrap();
5638 assert!(
5639 engine.score.parts[0].staves[0].measures[0].voices[0][0]
5640 .chord_symbol
5641 .as_ref()
5642 .and_then(|chord| chord.range_end.as_ref())
5643 .is_some()
5644 );
5645 engine.undo().unwrap();
5646 assert!(
5647 engine.score.parts[0].staves[0].measures[0].voices[0][0]
5648 .chord_symbol
5649 .as_ref()
5650 .is_some_and(|chord| chord.range_end.is_none())
5651 );
5652 }
5653
5654 #[test]
5655 fn set_harp_pedal_diagrams_is_undoable_and_json_compatible() {
5656 let mut engine = ScoreEngine::new();
5657 let mut diagram = HarpPedalDiagram::default();
5658 diagram.positions[0] = super::super::score::HarpPedalPosition::Flat;
5659 diagram.positions[6] = super::super::score::HarpPedalPosition::Sharp;
5660 let command = Command::SetHarpPedalDiagrams(SetHarpPedalDiagramsCmd {
5661 part_index: 0,
5662 staff_index: 0,
5663 measure_index: 0,
5664 diagrams: vec![diagram.clone()],
5665 });
5666 let restored: Command =
5667 serde_json::from_str(&serde_json::to_string(&command).unwrap()).unwrap();
5668 engine.apply(restored).unwrap();
5669 assert_eq!(
5670 engine.score.parts[0].staves[0].measures[0].harp_pedal_diagrams,
5671 vec![diagram]
5672 );
5673 engine.undo().unwrap();
5674 assert!(
5675 engine.score.parts[0].staves[0].measures[0]
5676 .harp_pedal_diagrams
5677 .is_empty()
5678 );
5679 }
5680
5681 #[test]
5682 fn set_note_placement_is_undoable_and_rejects_non_finite_values() {
5683 let mut score = default_engine_score();
5684 score.parts[0].staves[0].measures[0].voices[0] =
5685 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5686 let mut engine = ScoreEngine::new();
5687 engine.replace_score(score);
5688 let command = Command::SetNotePlacement(SetNotePlacementCmd {
5689 part_index: 0,
5690 staff_index: 0,
5691 measure_index: 0,
5692 voice: 0,
5693 note_index: 0,
5694 offset_x: Some(12.5),
5695 offset_y: Some(-3.0),
5696 relative_x: Some(1.25),
5697 relative_y: Some(-0.5),
5698 });
5699 engine.apply(command).unwrap();
5700 let note = &engine.score.parts[0].staves[0].measures[0].voices[0][0];
5701 assert_eq!(note.offset_x, Some(12.5));
5702 assert_eq!(note.relative_y, Some(-0.5));
5703 engine.undo().unwrap();
5704 assert_eq!(
5705 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
5706 None
5707 );
5708 let invalid = Command::SetNotePlacement(SetNotePlacementCmd {
5709 part_index: 0,
5710 staff_index: 0,
5711 measure_index: 0,
5712 voice: 0,
5713 note_index: 0,
5714 offset_x: Some(f64::NAN),
5715 offset_y: None,
5716 relative_x: None,
5717 relative_y: None,
5718 });
5719 assert!(engine.apply(invalid).is_err());
5720 assert_eq!(
5721 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
5722 None
5723 );
5724 }
5725
5726 #[test]
5727 fn set_guitar_bend_alter_updates_note_and_can_clear() {
5728 let mut score = default_engine_score();
5729 apply_command(
5730 &Command::AddNote(AddNoteCmd {
5731 part_index: 0,
5732 staff_index: 0,
5733 measure_index: 0,
5734 voice: 0,
5735 position: 0,
5736 pitch: Some(Pitch::new(Step::G, 4)),
5737 duration: Duration::Quarter,
5738 dot_count: 0,
5739 is_rest: false,
5740 tuplet: None,
5741 }),
5742 &mut score,
5743 )
5744 .unwrap();
5745 let location = SetGuitarBendAlterCmd {
5746 part_index: 0,
5747 staff_index: 0,
5748 measure_index: 0,
5749 voice: 0,
5750 note_index: 0,
5751 alter_cents: Some(200),
5752 };
5753 apply_command(&Command::SetGuitarBendAlter(location.clone()), &mut score).unwrap();
5754 assert_eq!(
5755 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
5756 Some(200)
5757 );
5758 let mut cleared = location;
5759 cleared.alter_cents = None;
5760 apply_command(&Command::SetGuitarBendAlter(cleared), &mut score).unwrap();
5761 assert_eq!(
5762 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
5763 None
5764 );
5765 }
5766
5767 #[test]
5768 fn set_guitar_bend_curve_is_undoable_and_json_compatible() {
5769 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5770 let command = Command::SetGuitarBendCurve(SetGuitarBendCurveCmd {
5771 part_index: 0,
5772 staff_index: 0,
5773 measure_index: 0,
5774 voice: 0,
5775 note_index: 0,
5776 points: vec![
5777 crate::GuitarBendPoint {
5778 position_per_mille: 0,
5779 alter_cents: 0,
5780 },
5781 crate::GuitarBendPoint {
5782 position_per_mille: 500,
5783 alter_cents: 200,
5784 },
5785 crate::GuitarBendPoint {
5786 position_per_mille: 1000,
5787 alter_cents: 0,
5788 },
5789 ],
5790 });
5791 let decoded: Command =
5792 serde_json::from_str(&serde_json::to_string(&command).unwrap()).unwrap();
5793 let mut stack = CommandStack::new(16);
5794 stack.execute(decoded, &mut score).unwrap();
5795 assert_eq!(
5796 score.parts[0].staves[0].measures[0].voices[0][0]
5797 .guitar_bend_curve
5798 .len(),
5799 3
5800 );
5801 stack.undo(&mut score).unwrap();
5802 assert!(
5803 score.parts[0].staves[0].measures[0].voices[0][0]
5804 .guitar_bend_curve
5805 .is_empty()
5806 );
5807 }
5808
5809 #[test]
5810 fn set_duration_updates_note_and_preserves_measure_capacity() {
5811 let mut score = default_engine_score();
5812 apply_command(
5813 &Command::AddNote(AddNoteCmd {
5814 part_index: 0,
5815 staff_index: 0,
5816 measure_index: 0,
5817 voice: 0,
5818 position: 0,
5819 pitch: Some(Pitch::new(Step::C, 4)),
5820 duration: Duration::Quarter,
5821 dot_count: 0,
5822 is_rest: false,
5823 tuplet: None,
5824 }),
5825 &mut score,
5826 )
5827 .unwrap();
5828 apply_command(
5829 &Command::SetDuration(SetDurationCmd {
5830 part_index: 0,
5831 staff_index: 0,
5832 measure_index: 0,
5833 voice: 0,
5834 note_index: 0,
5835 duration: Duration::Half,
5836 dot_count: 1,
5837 }),
5838 &mut score,
5839 )
5840 .unwrap();
5841 let voice = &score.parts[0].staves[0].measures[0].voices[0];
5842 assert_eq!(voice[0].duration, Duration::Half);
5843 assert_eq!(voice[0].dot_count, 1);
5844 assert!((voice.iter().map(|note| note.beats()).sum::<f64>() - 4.0).abs() < 1e-9);
5845 }
5846
5847 #[test]
5848 fn set_tempo_updates_score() {
5849 let mut score = default_engine_score();
5850 let cmd = Command::SetTempo(SetTempoCmd { bpm: 160 });
5851 apply_command(&cmd, &mut score).unwrap();
5852 assert_eq!(score.settings.tempo_bpm, 160);
5853 }
5854
5855 #[test]
5856 fn add_measure_increases_count() {
5857 let mut score = default_engine_score();
5858 let before = score.measure_count();
5859 apply_command(
5860 &Command::AddMeasure(AddMeasureCmd { after_index: 0 }),
5861 &mut score,
5862 )
5863 .unwrap();
5864 assert_eq!(score.measure_count(), before + 1);
5865 }
5866
5867 #[test]
5868 fn delete_measure_decreases_count() {
5869 let mut score = default_engine_score();
5870 let before = score.measure_count();
5871 apply_command(
5872 &Command::DeleteMeasure(DeleteMeasureCmd { measure_index: 0 }),
5873 &mut score,
5874 )
5875 .unwrap();
5876 assert_eq!(score.measure_count(), before - 1);
5877 }
5878
5879 #[test]
5880 fn undo_restores_score() {
5881 let mut stack = CommandStack::new(50);
5882 let mut score = default_engine_score();
5883 let before = score.settings.tempo_bpm;
5884 stack
5885 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
5886 .unwrap();
5887 assert_eq!(score.settings.tempo_bpm, 200);
5888 stack.undo(&mut score).unwrap();
5889 assert_eq!(score.settings.tempo_bpm, before);
5890 }
5891
5892 #[test]
5893 fn redo_reapplies_command() {
5894 let mut stack = CommandStack::new(50);
5895 let mut score = default_engine_score();
5896 stack
5897 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
5898 .unwrap();
5899 stack.undo(&mut score).unwrap();
5900 stack.redo(&mut score).unwrap();
5901 assert_eq!(score.settings.tempo_bpm, 200);
5902 }
5903
5904 #[test]
5905 fn undo_nothing_returns_error() {
5906 let mut stack = CommandStack::new(50);
5907 let mut score = default_engine_score();
5908 assert!(stack.undo(&mut score).is_err());
5909 }
5910
5911 #[test]
5912 fn add_part_appends_part() {
5913 let mut score = default_engine_score();
5914 let before = score.parts.len();
5915 apply_command(
5916 &Command::AddPart(AddPartCmd {
5917 name: "Violin".into(),
5918 short_name: "Vln.".into(),
5919 clefs: vec!["Treble".into()],
5920 midi_channel: 0,
5921 midi_program: 0,
5922 }),
5923 &mut score,
5924 )
5925 .unwrap();
5926 assert_eq!(score.parts.len(), before + 1);
5927 }
5928
5929 #[test]
5930 fn delete_part_removes_part() {
5931 let mut score = default_engine_score();
5932 apply_command(
5933 &Command::AddPart(AddPartCmd {
5934 name: "Violin".into(),
5935 short_name: "V.".into(),
5936 clefs: vec!["Treble".into()],
5937 midi_channel: 0,
5938 midi_program: 0,
5939 }),
5940 &mut score,
5941 )
5942 .unwrap();
5943 let before = score.parts.len();
5944 apply_command(
5945 &Command::DeletePart(DeletePartCmd { part_index: 0 }),
5946 &mut score,
5947 )
5948 .unwrap();
5949 assert_eq!(score.parts.len(), before - 1);
5950 }
5951
5952 #[test]
5953 fn delete_part_out_of_range_returns_err() {
5954 let mut score = default_engine_score();
5955 assert!(
5956 apply_command(
5957 &Command::DeletePart(DeletePartCmd { part_index: 99 }),
5958 &mut score
5959 )
5960 .is_err()
5961 );
5962 }
5963
5964 #[test]
5965 fn delete_part_undo_restores_part() {
5966 let mut stack = CommandStack::new(50);
5967 let mut score = default_engine_score();
5968 apply_command(
5969 &Command::AddPart(AddPartCmd {
5970 name: "Violin".into(),
5971 short_name: "V.".into(),
5972 clefs: vec!["Treble".into()],
5973 midi_channel: 0,
5974 midi_program: 0,
5975 }),
5976 &mut score,
5977 )
5978 .unwrap();
5979 let before = score.parts.len();
5980 stack
5981 .execute(
5982 Command::DeletePart(DeletePartCmd { part_index: 0 }),
5983 &mut score,
5984 )
5985 .unwrap();
5986 assert_eq!(score.parts.len(), before - 1);
5987 stack.undo(&mut score).unwrap();
5988 assert_eq!(score.parts.len(), before);
5989 }
5990
5991 #[test]
5992 fn reorder_parts_remaps_linked_views_and_preserves_undo() {
5993 let mut engine = crate::ScoreEngine::new();
5994 engine
5995 .apply(Command::AddPart(AddPartCmd {
5996 name: "Flute".into(),
5997 short_name: "Fl.".into(),
5998 clefs: vec!["Treble".into()],
5999 midi_channel: 1,
6000 midi_program: 73,
6001 }))
6002 .unwrap();
6003 engine
6004 .score
6005 .views
6006 .push(ScoreView::linked_part("flute", "Flute", 1));
6007 engine.score.part_groups.push(PartGroup {
6008 first_part: 0,
6009 last_part: 1,
6010 symbol: super::super::score::PartGroupSymbol::Bracket,
6011 barlines_connect: false,
6012 });
6013
6014 engine
6015 .apply(Command::ReorderParts(ReorderPartsCmd { order: vec![1, 0] }))
6016 .unwrap();
6017 assert_eq!(engine.score.parts[0].name, "Flute");
6018 assert_eq!(engine.score.views[0].parts, vec![0]);
6019 engine.undo().unwrap();
6020 assert_eq!(engine.score.parts[0].name, "Piano");
6021 assert_eq!(engine.score.views[0].parts, vec![1]);
6022 }
6023
6024 #[test]
6025 fn reorder_parts_rejects_splitting_a_part_group_without_mutation() {
6026 let mut score = Score::template(ScoreTemplate::StringQuartet);
6027 score.part_groups.push(PartGroup {
6028 first_part: 0,
6029 last_part: 1,
6030 symbol: super::super::score::PartGroupSymbol::Bracket,
6031 barlines_connect: false,
6032 });
6033 let before = serde_json::to_value(&score).unwrap();
6034
6035 assert!(
6036 apply_command(
6037 &Command::ReorderParts(ReorderPartsCmd {
6038 order: vec![0, 2, 1, 3],
6039 }),
6040 &mut score,
6041 )
6042 .is_err()
6043 );
6044 assert_eq!(serde_json::to_value(&score).unwrap(), before);
6045 }
6046
6047 #[test]
6048 fn set_metadata_updates_title() {
6049 let mut score = default_engine_score();
6050 apply_command(
6051 &Command::SetMetadata(SetMetadataCmd {
6052 title: Some("New Title".into()),
6053 ..Default::default()
6054 }),
6055 &mut score,
6056 )
6057 .unwrap();
6058 assert_eq!(score.metadata.title, "New Title");
6059 }
6060
6061 #[test]
6062 fn set_metadata_none_fields_skipped() {
6063 let mut score = default_engine_score();
6064 let original_composer = score.metadata.composer.clone();
6065 apply_command(
6066 &Command::SetMetadata(SetMetadataCmd {
6067 title: Some("X".into()),
6068 ..Default::default()
6069 }),
6070 &mut score,
6071 )
6072 .unwrap();
6073 assert_eq!(score.metadata.composer, original_composer);
6074 }
6075
6076 #[test]
6077 fn set_volta_sets_bracket() {
6078 use crate::model::score::VoltaBracket;
6079 let mut score = default_engine_score();
6080 let volta = VoltaBracket {
6081 number: 1,
6082 kind: "begin_end".into(),
6083 numbers: Vec::new(),
6084 };
6085 apply_command(
6086 &Command::SetVolta(SetVoltaCmd {
6087 measure_index: 0,
6088 volta: Some(volta.clone()),
6089 }),
6090 &mut score,
6091 )
6092 .unwrap();
6093 assert!(score.parts[0].staves[0].measures[0].volta.is_some());
6094 }
6095
6096 #[test]
6097 fn set_volta_none_clears_bracket() {
6098 use crate::model::score::VoltaBracket;
6099 let mut score = default_engine_score();
6100 score.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
6101 number: 1,
6102 kind: "begin_end".into(),
6103 numbers: Vec::new(),
6104 });
6105 apply_command(
6106 &Command::SetVolta(SetVoltaCmd {
6107 measure_index: 0,
6108 volta: None,
6109 }),
6110 &mut score,
6111 )
6112 .unwrap();
6113 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
6114 }
6115
6116 #[test]
6117 fn set_volta_undo_restores_old() {
6118 use crate::model::score::VoltaBracket;
6119 let mut stack = CommandStack::new(50);
6120 let mut score = default_engine_score();
6121 stack
6122 .execute(
6123 Command::SetVolta(SetVoltaCmd {
6124 measure_index: 0,
6125 volta: Some(VoltaBracket {
6126 number: 1,
6127 kind: "begin_end".into(),
6128 numbers: Vec::new(),
6129 }),
6130 }),
6131 &mut score,
6132 )
6133 .unwrap();
6134 stack.undo(&mut score).unwrap();
6135 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
6136 }
6137
6138 #[test]
6139 fn set_clef_updates_staff_clef() {
6140 use crate::model::notation::Clef;
6141 let mut score = default_engine_score();
6142 apply_command(
6143 &Command::SetClef(SetClefCmd {
6144 part_index: 0,
6145 staff_index: 0,
6146 clef: Clef::Bass,
6147 }),
6148 &mut score,
6149 )
6150 .unwrap();
6151 assert_eq!(score.parts[0].staves[0].clef, Clef::Bass);
6152 }
6153
6154 #[test]
6155 fn set_clef_out_of_range_returns_err() {
6156 use crate::model::notation::Clef;
6157 let mut score = default_engine_score();
6158 assert!(
6159 apply_command(
6160 &Command::SetClef(SetClefCmd {
6161 part_index: 99,
6162 staff_index: 0,
6163 clef: Clef::Bass,
6164 }),
6165 &mut score
6166 )
6167 .is_err()
6168 );
6169 }
6170
6171 #[test]
6172 fn set_part_name_updates_name() {
6173 let mut score = default_engine_score();
6174 apply_command(
6175 &Command::SetPartName(SetPartNameCmd {
6176 part_index: 0,
6177 name: "Violin".into(),
6178 short_name: "Vln.".into(),
6179 }),
6180 &mut score,
6181 )
6182 .unwrap();
6183 assert_eq!(score.parts[0].name, "Violin");
6184 assert_eq!(score.parts[0].short_name, "Vln.");
6185 }
6186
6187 #[test]
6188 fn set_part_name_undo_restores_old() {
6189 let mut stack = CommandStack::new(50);
6190 let mut score = default_engine_score();
6191 let original = score.parts[0].name.clone();
6192 stack
6193 .execute(
6194 Command::SetPartName(SetPartNameCmd {
6195 part_index: 0,
6196 name: "Flute".into(),
6197 short_name: "Fl.".into(),
6198 }),
6199 &mut score,
6200 )
6201 .unwrap();
6202 stack.undo(&mut score).unwrap();
6203 assert_eq!(score.parts[0].name, original);
6204 }
6205
6206 #[test]
6207 fn set_metadata_undo_restores_old_title() {
6208 let mut stack = CommandStack::new(50);
6209 let mut score = default_engine_score();
6210 let original = score.metadata.title.clone();
6211 stack
6212 .execute(
6213 Command::SetMetadata(SetMetadataCmd {
6214 title: Some("Changed".into()),
6215 ..Default::default()
6216 }),
6217 &mut score,
6218 )
6219 .unwrap();
6220 assert_ne!(score.metadata.title, original);
6221 stack.undo(&mut score).unwrap();
6222 assert_eq!(score.metadata.title, original);
6223 }
6224
6225 #[test]
6226 fn set_midi_instrument_updates_channel_and_program() {
6227 let mut score = default_engine_score();
6228 apply_command(
6229 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
6230 part_index: 0,
6231 midi_channel: 2,
6232 midi_program: 40,
6233 }),
6234 &mut score,
6235 )
6236 .unwrap();
6237 assert_eq!(score.parts[0].midi_channel, 2);
6238 assert_eq!(score.parts[0].midi_program, 40);
6239 }
6240
6241 #[test]
6242 fn set_midi_instrument_clamps_channel_to_15() {
6243 let mut score = default_engine_score();
6244 apply_command(
6245 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
6246 part_index: 0,
6247 midi_channel: 20,
6248 midi_program: 0,
6249 }),
6250 &mut score,
6251 )
6252 .unwrap();
6253 assert_eq!(score.parts[0].midi_channel, 15);
6254 }
6255
6256 #[test]
6257 fn set_midi_instrument_undo_restores_old() {
6258 let mut stack = CommandStack::new(50);
6259 let mut score = default_engine_score();
6260 score.parts[0].midi_channel = 3;
6261 score.parts[0].midi_program = 10;
6262 stack
6263 .execute(
6264 Command::SetMidiInstrument(SetMidiInstrumentCmd {
6265 part_index: 0,
6266 midi_channel: 9,
6267 midi_program: 114,
6268 }),
6269 &mut score,
6270 )
6271 .unwrap();
6272 stack.undo(&mut score).unwrap();
6273 assert_eq!(score.parts[0].midi_channel, 3);
6274 assert_eq!(score.parts[0].midi_program, 10);
6275 }
6276
6277 #[test]
6278 fn set_transpose_updates_staff() {
6279 let mut score = default_engine_score();
6280 apply_command(
6281 &Command::SetTranspose(SetTransposeCmd {
6282 part_index: 0,
6283 staff_index: 0,
6284 semitones: -2,
6285 }),
6286 &mut score,
6287 )
6288 .unwrap();
6289 assert_eq!(score.parts[0].staves[0].transpose_semitones, -2);
6290 }
6291
6292 #[test]
6293 fn set_transpose_out_of_range_returns_err() {
6294 let mut score = default_engine_score();
6295 assert!(
6296 apply_command(
6297 &Command::SetTranspose(SetTransposeCmd {
6298 part_index: 99,
6299 staff_index: 0,
6300 semitones: -2,
6301 }),
6302 &mut score
6303 )
6304 .is_err()
6305 );
6306 }
6307
6308 #[test]
6309 fn transpose_staff_region_is_undoable_and_json_compatible() {
6310 let mut stack = CommandStack::new(50);
6311 let mut score = Score::new("Region", 120, 4, 4, 0, 2);
6312 for measure in &mut score.parts[0].staves[0].measures {
6313 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
6314 }
6315 let command = Command::TransposeStaffRegion(TransposeStaffRegionCmd {
6316 part_index: 0,
6317 staff_index: 0,
6318 start_measure: 1,
6319 end_measure: 2,
6320 semitones: 2,
6321 target: RegionalTranspositionTarget::Written,
6322 });
6323 let json = serde_json::to_string(&command).expect("command serializes");
6324 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6325 assert_eq!(command_key(&decoded), "TransposeStaffRegion");
6326 stack.execute(decoded, &mut score).expect("region applies");
6327 assert_eq!(
6328 score.parts[0].staves[0].measures[0].voices[0][0].pitches[0].to_midi(),
6329 60
6330 );
6331 assert_eq!(
6332 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0].to_midi(),
6333 62
6334 );
6335 stack.undo(&mut score).expect("region undo applies");
6336 assert_eq!(
6337 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0].to_midi(),
6338 60
6339 );
6340 }
6341
6342 #[test]
6343 fn set_tempo_at_measure_sets_tempo() {
6344 let mut score = default_engine_score();
6345 apply_command(
6346 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
6347 measure_index: 0,
6348 bpm: Some(80),
6349 }),
6350 &mut score,
6351 )
6352 .unwrap();
6353 assert_eq!(score.parts[0].staves[0].measures[0].tempo, Some(80));
6354 }
6355
6356 #[test]
6357 fn set_tempo_at_measure_none_clears_tempo() {
6358 let mut score = default_engine_score();
6359 score.parts[0].staves[0].measures[0].tempo = Some(120);
6360 apply_command(
6361 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
6362 measure_index: 0,
6363 bpm: None,
6364 }),
6365 &mut score,
6366 )
6367 .unwrap();
6368 assert!(score.parts[0].staves[0].measures[0].tempo.is_none());
6369 }
6370
6371 #[test]
6372 fn set_tempo_ramp_at_measure_applies() {
6373 let mut score = default_engine_score();
6374 apply_command(
6375 &Command::SetTempoRampAtMeasure(SetTempoRampAtMeasureCmd {
6376 measure_index: 0,
6377 target_bpm: Some(72),
6378 }),
6379 &mut score,
6380 )
6381 .expect("tempo ramp applies");
6382 assert_eq!(score.parts[0].staves[0].measures[0].tempo_ramp_to, Some(72));
6383 }
6384
6385 #[test]
6388 fn batch_execute_two_commands_single_undo() {
6389 let mut stack = CommandStack::new(50);
6390 let mut score = default_engine_score();
6391 let original_bpm = score.settings.tempo_bpm;
6392 stack
6393 .batch_execute(
6394 vec![
6395 Command::SetTempo(SetTempoCmd { bpm: 160 }),
6396 Command::SetTempo(SetTempoCmd { bpm: 180 }),
6397 ],
6398 &mut score,
6399 )
6400 .unwrap();
6401 assert_eq!(score.settings.tempo_bpm, 180);
6402 stack.undo(&mut score).unwrap();
6403 assert_eq!(score.settings.tempo_bpm, original_bpm);
6404 }
6405
6406 #[test]
6407 fn batch_execute_partial_failure_rollback() {
6408 let mut stack = CommandStack::new(50);
6409 let mut score = default_engine_score();
6410 let original_bpm = score.settings.tempo_bpm;
6411 let result = stack.batch_execute(
6412 vec![
6413 Command::SetTempo(SetTempoCmd { bpm: 160 }),
6414 Command::DeleteNote(DeleteNoteCmd {
6415 note_id: "nonexistent".into(),
6416 part_index: 99,
6417 staff_index: 0,
6418 measure_index: 0,
6419 voice: 0,
6420 }),
6421 ],
6422 &mut score,
6423 );
6424 assert!(result.is_err());
6425 assert_eq!(score.settings.tempo_bpm, original_bpm);
6426 }
6427
6428 #[test]
6429 fn execute_rejects_invalid_candidate_without_mutating_or_recording_history() {
6430 let mut stack = CommandStack::new(50);
6431 let mut score = default_engine_score();
6432 score.parts[0].staves[0].tablature = Some(crate::TablatureConfig {
6433 lines: 6,
6434 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6435 capo: 0,
6436 });
6437 stack
6438 .execute(
6439 Command::AddNote(AddNoteCmd {
6440 part_index: 0,
6441 staff_index: 0,
6442 measure_index: 0,
6443 voice: 0,
6444 position: 0,
6445 pitch: Some(Pitch::new(Step::C, 4)),
6446 duration: Duration::Quarter,
6447 dot_count: 0,
6448 is_rest: false,
6449 tuplet: None,
6450 }),
6451 &mut score,
6452 )
6453 .expect("valid pitched note should be accepted");
6454 let original = score.clone();
6455 let result = stack.execute(
6456 Command::SetTabPosition(SetTabPositionCmd {
6457 part_index: 0,
6458 staff_index: 0,
6459 measure_index: 0,
6460 voice: 0,
6461 note_index: 0,
6462 position: Some(crate::TabPosition { string: 7, fret: 0 }),
6463 }),
6464 &mut score,
6465 );
6466 assert!(matches!(result, Err(Error::InvalidScore)));
6467 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
6468 let original_note = &original.parts[0].staves[0].measures[0].voices[0][0];
6469 assert_eq!(note.pitches, original_note.pitches);
6470 assert_eq!(note.duration, original_note.duration);
6471 assert_eq!(note.tab_position, original_note.tab_position);
6472 assert_eq!(note.tab_positions, original_note.tab_positions);
6473 assert!(stack.can_undo());
6474 }
6475
6476 #[test]
6477 fn set_tablature_config_is_undoable_and_json_compatible() {
6478 let mut stack = CommandStack::new(50);
6479 let mut score = default_engine_score();
6480 let config = crate::TablatureConfig {
6481 lines: 6,
6482 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6483 capo: 2,
6484 };
6485 let command = Command::SetTablatureConfig(SetTablatureConfigCmd {
6486 part_index: 0,
6487 staff_index: 0,
6488 config: Some(config.clone()),
6489 });
6490 let json = serde_json::to_string(&command).expect("command should serialize");
6491 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6492 assert_eq!(command_key(&decoded), "SetTablatureConfig");
6493 stack
6494 .execute(decoded, &mut score)
6495 .expect("config should apply");
6496 assert_eq!(score.parts[0].staves[0].tablature, Some(config));
6497 assert_eq!(
6498 score.parts[0].staves[0].presentation.kind,
6499 StaffKind::Tablature
6500 );
6501 stack.undo(&mut score).expect("config undo should apply");
6502 assert!(score.parts[0].staves[0].tablature.is_none());
6503 assert_eq!(
6504 score.parts[0].staves[0].presentation.kind,
6505 StaffKind::Standard
6506 );
6507 stack.redo(&mut score).expect("config redo should apply");
6508 assert_eq!(
6509 score.parts[0].staves[0]
6510 .tablature
6511 .as_ref()
6512 .map(|tab| tab.capo),
6513 Some(2)
6514 );
6515 }
6516
6517 #[test]
6518 fn set_staff_presentation_is_undoable_and_requires_tablature_config() {
6519 let mut stack = CommandStack::new(50);
6520 let mut score = default_engine_score();
6521 let tab = StaffPresentation {
6522 kind: StaffKind::Tablature,
6523 lines: 6,
6524 line_distance: 1.25,
6525 small: true,
6526 cutaway: false,
6527 visible: true,
6528 notehead_scheme: super::super::score::StaffNoteheadScheme::Standard,
6529 tablature_rhythm_display: super::super::score::TablatureRhythmDisplay::FretOnly,
6530 tablature_fret_mark_style: super::super::score::TablatureFretMarkStyle::Arabic,
6531 };
6532 let rejected = Command::SetStaffPresentation(SetStaffPresentationCmd {
6533 part_index: 0,
6534 staff_index: 0,
6535 presentation: tab.clone(),
6536 });
6537 let before = score.clone();
6538 assert!(stack.execute(rejected, &mut score).is_err());
6539 assert_eq!(
6540 score.parts[0].staves[0].presentation,
6541 before.parts[0].staves[0].presentation
6542 );
6543 assert!(!stack.can_undo());
6544
6545 stack
6546 .execute(
6547 Command::SetTablatureConfig(SetTablatureConfigCmd {
6548 part_index: 0,
6549 staff_index: 0,
6550 config: Some(crate::TablatureConfig {
6551 lines: 6,
6552 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6553 capo: 0,
6554 }),
6555 }),
6556 &mut score,
6557 )
6558 .expect("tablature configuration should apply");
6559 stack
6560 .execute(
6561 Command::SetStaffPresentation(SetStaffPresentationCmd {
6562 part_index: 0,
6563 staff_index: 0,
6564 presentation: tab.clone(),
6565 }),
6566 &mut score,
6567 )
6568 .expect("staff presentation should apply");
6569 assert_eq!(stack.undo_key().as_deref(), Some("SetStaffPresentation"));
6570 assert_eq!(score.parts[0].staves[0].presentation, tab);
6571 stack
6572 .undo(&mut score)
6573 .expect("presentation undo should apply");
6574 assert_eq!(
6575 score.parts[0].staves[0].presentation.kind,
6576 StaffKind::Tablature
6577 );
6578 stack
6579 .redo(&mut score)
6580 .expect("presentation redo should apply");
6581 assert_eq!(
6582 score.parts[0].staves[0].presentation.kind,
6583 StaffKind::Tablature
6584 );
6585 }
6586
6587 #[test]
6588 fn set_instrument_definition_is_undoable_and_rejects_invalid_ranges() {
6589 let mut stack = CommandStack::new(50);
6590 let mut score = default_engine_score();
6591 let mut invalid = InstrumentDefinition::new("violin", "Violin");
6592 invalid.written_range = Some(InstrumentRange {
6593 lowest: 100,
6594 highest: 55,
6595 });
6596 let before = score.clone();
6597 assert!(
6598 stack
6599 .execute(
6600 Command::SetInstrumentDefinition(SetInstrumentDefinitionCmd {
6601 part_index: 0,
6602 definition: Some(invalid),
6603 }),
6604 &mut score,
6605 )
6606 .is_err()
6607 );
6608 assert_eq!(score.parts[0].instrument, before.parts[0].instrument);
6609
6610 let mut definition = InstrumentDefinition::new("violin", "Violin");
6611 definition.short_name = "Vln.".to_string();
6612 definition.family = Some("strings".to_string());
6613 definition.written_range = Some(InstrumentRange {
6614 lowest: 55,
6615 highest: 103,
6616 });
6617 definition.sounding_range = definition.written_range;
6618 definition.default_clefs = vec![Clef::Treble];
6619 definition.midi_program = 40;
6620 let command = Command::SetInstrumentDefinition(SetInstrumentDefinitionCmd {
6621 part_index: 0,
6622 definition: Some(definition.clone()),
6623 });
6624 let json = serde_json::to_string(&command).expect("command should serialize");
6625 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6626 assert_eq!(command_key(&decoded), "SetInstrumentDefinition");
6627 stack
6628 .execute(decoded, &mut score)
6629 .expect("definition applies");
6630 assert_eq!(score.parts[0].instrument, Some(definition));
6631 stack.undo(&mut score).expect("definition undo applies");
6632 assert!(score.parts[0].instrument.is_none());
6633 stack.redo(&mut score).expect("definition redo applies");
6634 assert_eq!(
6635 score.parts[0]
6636 .instrument
6637 .as_ref()
6638 .map(|value| value.id.as_str()),
6639 Some("violin")
6640 );
6641 }
6642
6643 #[test]
6644 fn set_percussion_kit_is_undoable_and_rejects_invalid_entries() {
6645 let mut stack = CommandStack::new(50);
6646 let mut score = default_engine_score();
6647 let invalid = Command::SetPercussionKit(SetPercussionKitCmd {
6648 part_index: 0,
6649 instruments: vec![PercussionInstrument {
6650 id: "snare".to_string(),
6651 name: None,
6652 midi_unpitched: Some(38),
6653 staff_position: Some(33),
6654 notehead: None,
6655 preferred_voice: None,
6656 techniques: Vec::new(),
6657 }],
6658 });
6659 assert!(stack.execute(invalid, &mut score).is_err());
6660 assert!(score.parts[0].percussion_instruments.is_empty());
6661
6662 let kit = vec![PercussionInstrument {
6663 id: "snare".to_string(),
6664 name: Some("Acoustic Snare".to_string()),
6665 midi_unpitched: Some(38),
6666 staff_position: Some(0),
6667 notehead: Some(NoteHead::Cross),
6668 preferred_voice: Some(1),
6669 techniques: vec!["rim-shot".to_string()],
6670 }];
6671 let command = Command::SetPercussionKit(SetPercussionKitCmd {
6672 part_index: 0,
6673 instruments: kit.clone(),
6674 });
6675 let json = serde_json::to_string(&command).expect("command serializes");
6676 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6677 assert_eq!(command_key(&decoded), "SetPercussionKit");
6678 stack.execute(decoded, &mut score).expect("kit applies");
6679 assert_eq!(score.parts[0].percussion_instruments, kit);
6680 stack.undo(&mut score).expect("kit undo applies");
6681 assert!(score.parts[0].percussion_instruments.is_empty());
6682 stack.redo(&mut score).expect("kit redo applies");
6683 assert_eq!(score.parts[0].percussion_instruments, kit);
6684 }
6685
6686 #[test]
6687 fn set_measure_instrument_change_is_undoable_and_json_compatible() {
6688 let mut stack = CommandStack::new(50);
6689 let mut score = default_engine_score();
6690 let mut definition = InstrumentDefinition::new("clarinet-bb", "B-flat Clarinet");
6691 definition.transpose_semitones = -2;
6692 definition.midi_program = 71;
6693 let command = Command::SetMeasureInstrumentChange(SetMeasureInstrumentChangeCmd {
6694 part_index: 0,
6695 staff_index: 0,
6696 measure_index: 2,
6697 definition: Some(definition.clone()),
6698 });
6699 let json = serde_json::to_string(&command).expect("command should serialize");
6700 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6701 assert_eq!(command_key(&decoded), "SetMeasureInstrumentChange");
6702 stack.execute(decoded, &mut score).expect("change applies");
6703 assert_eq!(
6704 score.parts[0].staves[0].measures[2].instrument_change,
6705 Some(definition)
6706 );
6707 stack.undo(&mut score).expect("undo applies");
6708 assert!(
6709 score.parts[0].staves[0].measures[2]
6710 .instrument_change
6711 .is_none()
6712 );
6713 }
6714
6715 #[test]
6716 fn set_measure_tablature_change_is_undoable_and_uses_base_line_count() {
6717 let mut stack = CommandStack::new(50);
6718 let mut score = default_engine_score();
6719 let base = crate::TablatureConfig {
6720 lines: 6,
6721 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6722 capo: 0,
6723 };
6724 score.parts[0].staves[0].tablature = Some(base.clone());
6725 let mut changed = base;
6726 changed.tuning_midi[0] = 38;
6727 changed.capo = 2;
6728 let command = Command::SetMeasureTablatureChange(SetMeasureTablatureChangeCmd {
6729 part_index: 0,
6730 staff_index: 0,
6731 measure_index: 2,
6732 config: Some(changed.clone()),
6733 });
6734 let json = serde_json::to_string(&command).expect("command serializes");
6735 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6736 assert_eq!(command_key(&decoded), "SetMeasureTablatureChange");
6737 stack.execute(decoded, &mut score).expect("change applies");
6738 assert_eq!(
6739 score.parts[0].staves[0].tablature_at(1),
6740 score.parts[0].staves[0].tablature
6741 );
6742 assert_eq!(score.parts[0].staves[0].tablature_at(2), Some(changed));
6743 stack.undo(&mut score).expect("change undo applies");
6744 assert!(
6745 score.parts[0].staves[0].measures[2]
6746 .tablature_change
6747 .is_none()
6748 );
6749
6750 let invalid = Command::SetMeasureTablatureChange(SetMeasureTablatureChangeCmd {
6751 part_index: 0,
6752 staff_index: 0,
6753 measure_index: 1,
6754 config: Some(crate::TablatureConfig {
6755 lines: 7,
6756 tuning_midi: vec![40, 45, 50, 55, 59, 64, 69],
6757 capo: 0,
6758 }),
6759 });
6760 assert!(stack.execute(invalid, &mut score).is_err());
6761 }
6762
6763 #[test]
6764 fn batch_execute_empty_is_noop() {
6765 let mut stack = CommandStack::new(50);
6766 let mut score = default_engine_score();
6767 stack.batch_execute(vec![], &mut score).unwrap();
6768 assert!(!stack.can_undo());
6769 }
6770
6771 #[test]
6774 fn batch_label_used_as_command_key() {
6775 let cmd = Command::Batch(BatchCmd {
6776 commands: vec![],
6777 label: Some("ApplyAI".to_string()),
6778 });
6779 assert_eq!(command_key(&cmd), "ApplyAI");
6780 }
6781
6782 #[test]
6783 fn batch_no_label_key_is_batch() {
6784 let cmd = Command::Batch(BatchCmd {
6785 commands: vec![],
6786 label: None,
6787 });
6788 assert_eq!(command_key(&cmd), "Batch");
6789 }
6790
6791 #[test]
6792 fn batch_label_survives_json_roundtrip() {
6793 let cmd = Command::Batch(BatchCmd {
6794 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 120 })],
6795 label: Some("PasteSelection".to_string()),
6796 });
6797 let json = serde_json::to_string(&cmd).unwrap();
6798 let cmd2: Command = serde_json::from_str(&json).unwrap();
6799 assert_eq!(command_key(&cmd2), "PasteSelection");
6800 }
6801
6802 #[test]
6803 fn batch_label_in_undo_key() {
6804 let mut stack = CommandStack::new(50);
6805 let mut score = default_engine_score();
6806 let cmd = Command::Batch(BatchCmd {
6807 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 140 })],
6808 label: Some("ApplyAI".to_string()),
6809 });
6810 stack.execute(cmd, &mut score).unwrap();
6811 assert_eq!(stack.undo_key(), Some("ApplyAI".to_string()));
6812 }
6813
6814 #[test]
6815 fn undo_returns_change_hint() {
6816 use crate::model::change_hint::ChangeScope;
6817 let mut stack = CommandStack::new(50);
6818 let mut score = default_engine_score();
6819 stack
6820 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
6821 .unwrap();
6822 let hint = stack.undo(&mut score).unwrap();
6823 assert_eq!(hint.scope, ChangeScope::Global);
6824 assert!(hint.playback_dirty);
6825 }
6826
6827 #[test]
6828 fn redo_returns_change_hint() {
6829 use crate::model::change_hint::ChangeScope;
6830 let mut stack = CommandStack::new(50);
6831 let mut score = default_engine_score();
6832 stack
6833 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
6834 .unwrap();
6835 stack.undo(&mut score).unwrap();
6836 let hint = stack.redo(&mut score).unwrap();
6837 assert_eq!(hint.scope, ChangeScope::Global);
6838 assert!(hint.playback_dirty);
6839 }
6840
6841 #[test]
6844 fn toggle_slur_sets_start_and_end() {
6845 let mut score = default_engine_score();
6846 let cmd = Command::AddNote(AddNoteCmd {
6847 part_index: 0,
6848 staff_index: 0,
6849 measure_index: 0,
6850 voice: 0,
6851 position: 0,
6852 pitch: Some(Pitch::new(Step::C, 4)),
6853 duration: Duration::Quarter,
6854 dot_count: 0,
6855 is_rest: false,
6856 tuplet: None,
6857 });
6858 apply_command(&cmd, &mut score).unwrap();
6859 apply_command(
6860 &Command::AddNote(AddNoteCmd {
6861 part_index: 0,
6862 staff_index: 0,
6863 measure_index: 0,
6864 voice: 0,
6865 position: 1,
6866 pitch: Some(Pitch::new(Step::D, 4)),
6867 duration: Duration::Quarter,
6868 dot_count: 0,
6869 is_rest: false,
6870 tuplet: None,
6871 }),
6872 &mut score,
6873 )
6874 .unwrap();
6875 let start = NoteAddr {
6876 part: 0,
6877 staff: 0,
6878 measure: 0,
6879 voice: 0,
6880 note: 0,
6881 };
6882 let end = NoteAddr {
6883 part: 0,
6884 staff: 0,
6885 measure: 0,
6886 voice: 0,
6887 note: 1,
6888 };
6889 apply_command(
6890 &Command::ToggleSlur(ToggleSlurCmd {
6891 start: start.clone(),
6892 end: end.clone(),
6893 }),
6894 &mut score,
6895 )
6896 .unwrap();
6897 assert!(score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
6898 assert!(score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
6899 apply_command(
6901 &Command::ToggleSlur(ToggleSlurCmd { start, end }),
6902 &mut score,
6903 )
6904 .unwrap();
6905 assert!(!score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
6906 assert!(!score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
6907 }
6908
6909 #[test]
6912 fn add_staff_appends_staff_with_correct_measure_count() {
6913 let mut score = default_engine_score();
6914 let before = score.parts[0].staves.len();
6915 let measure_count = score.parts[0].staves[0].measures.len();
6916 apply_command(
6917 &Command::AddStaff(AddStaffCmd {
6918 part_index: 0,
6919 clef: Clef::Bass,
6920 }),
6921 &mut score,
6922 )
6923 .unwrap();
6924 assert_eq!(score.parts[0].staves.len(), before + 1);
6925 let new_staff = score.parts[0].staves.last().unwrap();
6926 assert_eq!(new_staff.measures.len(), measure_count);
6927 }
6928
6929 #[test]
6930 fn add_staff_out_of_range_returns_err() {
6931 let mut score = default_engine_score();
6932 let result = apply_command(
6933 &Command::AddStaff(AddStaffCmd {
6934 part_index: 99,
6935 clef: Clef::Treble,
6936 }),
6937 &mut score,
6938 );
6939 assert!(result.is_err());
6940 }
6941
6942 #[test]
6943 fn delete_staff_removes_extra_staff() {
6944 let mut score = default_engine_score();
6945 apply_command(
6946 &Command::AddStaff(AddStaffCmd {
6947 part_index: 0,
6948 clef: Clef::Bass,
6949 }),
6950 &mut score,
6951 )
6952 .unwrap();
6953 assert_eq!(score.parts[0].staves.len(), 2);
6954 apply_command(
6955 &Command::DeleteStaff(DeleteStaffCmd {
6956 part_index: 0,
6957 staff_index: 1,
6958 }),
6959 &mut score,
6960 )
6961 .unwrap();
6962 assert_eq!(score.parts[0].staves.len(), 1);
6963 }
6964
6965 #[test]
6966 fn delete_last_staff_returns_err() {
6967 let mut score = default_engine_score();
6968 assert_eq!(score.parts[0].staves.len(), 1);
6969 let result = apply_command(
6970 &Command::DeleteStaff(DeleteStaffCmd {
6971 part_index: 0,
6972 staff_index: 0,
6973 }),
6974 &mut score,
6975 );
6976 assert!(result.is_err());
6977 }
6978
6979 #[test]
6982 fn set_tuplet_assigns_and_clears() {
6983 use crate::model::notation::TupletInfo;
6984 let mut score = default_engine_score();
6985 apply_command(
6986 &Command::AddNote(AddNoteCmd {
6987 part_index: 0,
6988 staff_index: 0,
6989 measure_index: 0,
6990 voice: 0,
6991 position: 0,
6992 pitch: Some(Pitch::new(Step::C, 4)),
6993 duration: Duration::Quarter,
6994 dot_count: 0,
6995 is_rest: false,
6996 tuplet: None,
6997 }),
6998 &mut score,
6999 )
7000 .unwrap();
7001 let ti = TupletInfo {
7002 actual_notes: 3,
7003 normal_notes: 2,
7004 };
7005 apply_command(
7006 &Command::SetTuplet(SetTupletCmd {
7007 part_index: 0,
7008 staff_index: 0,
7009 measure_index: 0,
7010 voice_index: 0,
7011 note_index: 0,
7012 tuplet: Some(ti.clone()),
7013 }),
7014 &mut score,
7015 )
7016 .unwrap();
7017 assert_eq!(
7018 score.parts[0].staves[0].measures[0].voices[0][0].tuplet,
7019 Some(ti)
7020 );
7021 apply_command(
7022 &Command::SetTuplet(SetTupletCmd {
7023 part_index: 0,
7024 staff_index: 0,
7025 measure_index: 0,
7026 voice_index: 0,
7027 note_index: 0,
7028 tuplet: None,
7029 }),
7030 &mut score,
7031 )
7032 .unwrap();
7033 assert!(
7034 score.parts[0].staves[0].measures[0].voices[0][0]
7035 .tuplet
7036 .is_none()
7037 );
7038 }
7039
7040 #[test]
7043 fn respell_score_cmd_changes_all_pitches() {
7044 use crate::model::pitch::Step;
7045 let mut score = default_engine_score();
7046 apply_command(
7047 &Command::AddNote(AddNoteCmd {
7048 part_index: 0,
7049 staff_index: 0,
7050 measure_index: 0,
7051 voice: 0,
7052 position: 0,
7053 pitch: Some(Pitch::with_alter(Step::C, 4, 1)), duration: Duration::Quarter,
7055 dot_count: 0,
7056 is_rest: false,
7057 tuplet: None,
7058 }),
7059 &mut score,
7060 )
7061 .unwrap();
7062 apply_command(
7063 &Command::RespellScore(RespellScoreCmd { prefer_flat: true }),
7064 &mut score,
7065 )
7066 .unwrap();
7067 let pitch = &score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
7068 assert_eq!(pitch.step, Step::D);
7069 assert_eq!(pitch.alter, -1); }
7071
7072 fn respell_fixture(measures: u32) -> Score {
7073 use crate::model::pitch::Step;
7074 let mut score = Score::new("Respell", 120, 4, 4, 0, measures);
7075 for measure in &mut score.parts[0].staves[0].measures {
7076 measure.voices[0] = vec![Note::new(Pitch::with_alter(Step::C, 4, 1), Duration::Whole)];
7077 }
7078 score
7079 }
7080
7081 fn spelled(score: &Score, measure: usize) -> (crate::model::pitch::Step, i8) {
7082 let pitch = &score.parts[0].staves[0].measures[measure].voices[0][0].pitches[0];
7083 (pitch.step.clone(), pitch.alter)
7084 }
7085
7086 #[test]
7087 fn respell_staff_region_changes_only_the_selection_and_is_undoable() {
7088 use crate::model::pitch::Step;
7089 let mut score = respell_fixture(4);
7090 let mut stack = CommandStack::new(50);
7091 let command = Command::RespellStaffRegion(RespellStaffRegionCmd {
7092 part_index: 0,
7093 staff_index: 0,
7094 start_measure: 1,
7095 end_measure: 3,
7096 policy: RespellPolicy::Flat,
7097 });
7098 let json = serde_json::to_string(&command).expect("command serializes");
7099 assert!(json.contains("\"policy\":\"flat\""));
7100 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7101 assert_eq!(command_key(&decoded), "RespellStaffRegion");
7102 assert_eq!(command_label(&decoded), "Respell Pitches (flat)");
7103 stack.execute(decoded, &mut score).expect("region respells");
7104 assert_eq!(spelled(&score, 0), (Step::C, 1));
7105 assert_eq!(spelled(&score, 1), (Step::D, -1));
7106 assert_eq!(spelled(&score, 2), (Step::D, -1));
7107 assert_eq!(spelled(&score, 3), (Step::C, 1));
7108 stack.undo(&mut score).expect("region respell undoes");
7109 assert!((0..4).all(|measure| spelled(&score, measure) == (Step::C, 1)));
7110 stack.redo(&mut score).expect("region respell redoes");
7111 assert_eq!(spelled(&score, 2), (Step::D, -1));
7112 }
7113
7114 #[test]
7115 fn respell_staff_region_keeps_ties_across_the_boundary_in_one_spelling() {
7116 use crate::model::pitch::Step;
7117 let mut score = respell_fixture(4);
7118 {
7119 let measures = &mut score.parts[0].staves[0].measures;
7120 measures[0].voices[0][0].tie_start = true;
7121 measures[1].voices[0][0].tie_end = true;
7122 measures[1].voices[0][0].tie_start = true;
7123 measures[2].voices[0][0].tie_end = true;
7124 }
7125 let changed = respell_staff_region(&mut score, 0, 0, 1, 2, RespellPolicy::Flat)
7126 .expect("tied region respells");
7127 assert_eq!(changed, (0, 2));
7128 assert_eq!(spelled(&score, 0), (Step::D, -1));
7129 assert_eq!(spelled(&score, 1), (Step::D, -1));
7130 assert_eq!(spelled(&score, 2), (Step::D, -1));
7131 assert_eq!(spelled(&score, 3), (Step::C, 1));
7132 }
7133
7134 #[test]
7135 fn respell_staff_region_follows_local_keys_and_skips_unpitched_notes() {
7136 use crate::model::notation::KeySignature;
7137 use crate::model::pitch::Step;
7138 let mut score = respell_fixture(4);
7139 for measure in &mut score.parts[0].staves[0].measures {
7140 measure.voices[0][0].pitches[0] = Pitch::with_alter(Step::D, 4, -1);
7141 }
7142 score.parts[0].staves[0].measures[2].key_sig = Some(KeySignature {
7143 fifths: -3,
7144 mode: "major".into(),
7145 });
7146 score.parts[0].staves[0].measures[1].voices[0][0].is_unpitched = true;
7147 respell_staff_region(&mut score, 0, 0, 0, 4, RespellPolicy::Key).expect("key respell");
7148 assert_eq!(spelled(&score, 0), (Step::C, 1));
7149 assert_eq!(
7150 spelled(&score, 1),
7151 (Step::D, -1),
7152 "unpitched staff position is kept"
7153 );
7154 assert_eq!(spelled(&score, 2), (Step::D, -1));
7155 assert_eq!(spelled(&score, 3), (Step::D, -1));
7156
7157 assert!(matches!(
7158 respell_staff_region(&mut score, 0, 0, 2, 2, RespellPolicy::Sharp),
7159 Err(Error::InvalidCommand(_))
7160 ));
7161 assert!(matches!(
7162 respell_staff_region(&mut score, 0, 5, 0, 1, RespellPolicy::Sharp),
7163 Err(Error::StaffNotFound(5))
7164 ));
7165 assert!(matches!(
7166 respell_staff_region(&mut score, 0, 0, 0, 5, RespellPolicy::Sharp),
7167 Err(Error::InvalidCommand(_))
7168 ));
7169 }
7170
7171 #[test]
7172 fn pickup_measure_length_limits_editing_and_survives_time_signature_changes() {
7173 use crate::model::score::MeasureLength;
7174 let mut score = Score::new("Pickup", 120, 4, 4, 0, 2);
7175 score.parts[0].staves[0].measures[0].actual_length = Some(MeasureLength {
7176 numerator: 1,
7177 denominator: 4,
7178 });
7179 score.parts[0].staves[0].measures[0].voices[0] = vec![Note::rest(Duration::Quarter)];
7180 assert!(validate(&score).is_valid());
7181 assert_eq!(
7182 crate::measure_beats_remaining(&score, 0, 0, 0, 0).expect("capacity"),
7183 0.0
7184 );
7185 let mut stack = CommandStack::new(50);
7186 stack
7187 .execute(
7188 Command::AddNote(AddNoteCmd {
7189 part_index: 0,
7190 staff_index: 0,
7191 measure_index: 0,
7192 voice: 0,
7193 position: 0,
7194 pitch: Some(Pitch::new(Step::G, 4)),
7195 duration: Duration::Quarter,
7196 dot_count: 0,
7197 is_rest: false,
7198 tuplet: None,
7199 }),
7200 &mut score,
7201 )
7202 .expect("note fits the pickup after trimming");
7203 let pickup = &score.parts[0].staves[0].measures[0].voices[0];
7204 assert_eq!(pickup.len(), 1);
7205 assert!(!pickup[0].is_rest);
7206
7207 stack
7208 .execute(
7209 Command::SetTimeSignature(SetTimeSignatureCmd {
7210 numerator: 3,
7211 denominator: 4,
7212 }),
7213 &mut score,
7214 )
7215 .expect("time signature changes");
7216 let beats: f64 = score.parts[0].staves[0].measures[0].voices[0]
7217 .iter()
7218 .map(|note| note.beats())
7219 .sum();
7220 assert!((beats - 1.0).abs() < 1e-9, "the pickup keeps one beat");
7221
7222 score.parts[0].staves[0].measures[1].actual_length = Some(MeasureLength {
7223 numerator: 0,
7224 denominator: 4,
7225 });
7226 assert!(validate(&score).errors.iter().any(|error| matches!(
7227 error,
7228 crate::ValidationError::InvalidMeasureLength { measure: 1, .. }
7229 )));
7230 }
7231
7232 #[test]
7233 fn set_lyric_addresses_verses_without_touching_verse_one() {
7234 use crate::model::notation::VerseLyric;
7235 let mut score = Score::new("Verses", 120, 4, 4, 0, 1);
7236 score.parts[0].staves[0].measures[0].voices[0] =
7237 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
7238 let lyric = |text: &str| Lyric {
7239 text: text.into(),
7240 syllabic: "single".into(),
7241 extend: false,
7242 };
7243 let set = |verse: Option<u8>, text: Option<&str>| {
7244 Command::SetLyric(SetLyricCmd {
7245 part_index: 0,
7246 staff_index: 0,
7247 measure_index: 0,
7248 voice: 0,
7249 note_index: 0,
7250 lyric: text.map(lyric),
7251 verse,
7252 })
7253 };
7254 let mut stack = CommandStack::new(50);
7255 stack
7256 .execute(set(None, Some("one")), &mut score)
7257 .expect("verse 1");
7258 stack
7259 .execute(set(Some(3), Some("three")), &mut score)
7260 .expect("verse 3");
7261 stack
7262 .execute(set(Some(2), Some("two")), &mut score)
7263 .expect("verse 2");
7264 stack
7265 .execute(set(Some(2), Some("TWO")), &mut score)
7266 .expect("replace verse 2");
7267 let note = |score: &Score| score.parts[0].staves[0].measures[0].voices[0][0].clone();
7268 assert_eq!(note(&score).lyric, Some(lyric("one")));
7269 assert_eq!(
7270 note(&score).additional_lyrics,
7271 vec![
7272 VerseLyric {
7273 verse: 2,
7274 lyric: lyric("TWO")
7275 },
7276 VerseLyric {
7277 verse: 3,
7278 lyric: lyric("three")
7279 }
7280 ]
7281 );
7282 stack
7283 .execute(set(Some(3), None), &mut score)
7284 .expect("clear verse 3");
7285 assert_eq!(note(&score).additional_lyrics.len(), 1);
7286 stack.undo(&mut score).expect("clear undoes");
7287 assert_eq!(note(&score).additional_lyrics.len(), 2);
7288
7289 let current = serde_json::to_string(&set(None, None)).expect("SetLyric serializes");
7291 assert!(current.contains("\"verse\":null"));
7292 let legacy: Command = serde_json::from_str(¤t.replace(",\"verse\":null", ""))
7293 .expect("legacy SetLyric JSON");
7294 stack
7295 .execute(legacy, &mut score)
7296 .expect("legacy clears verse 1");
7297 assert_eq!(note(&score).lyric, None);
7298 assert_eq!(note(&score).additional_lyrics.len(), 2);
7299
7300 assert!(matches!(
7301 apply_command(&set(Some(0), Some("zero")), &mut score),
7302 Err(Error::InvalidCommand(_))
7303 ));
7304 assert!(matches!(
7305 apply_command(&set(Some(33), Some("too far")), &mut score),
7306 Err(Error::InvalidCommand(_))
7307 ));
7308 score.parts[0].staves[0].measures[0].voices[0][0]
7309 .additional_lyrics
7310 .push(VerseLyric {
7311 verse: 2,
7312 lyric: lyric("duplicate"),
7313 });
7314 assert!(validate(&score).errors.iter().any(|error| matches!(
7315 error,
7316 crate::ValidationError::InvalidLyricVerse { verse: 2, .. }
7317 )));
7318 }
7319
7320 #[test]
7321 fn cycle_enharmonic_spelling_targets_one_chord_member_and_undoes() {
7322 let mut score = Score::new("Enharmonic", 120, 4, 4, 0, 1);
7323 let mut chord = Note::new(Pitch::with_alter(Step::C, 4, 1), Duration::Whole);
7324 chord.pitches.push(Pitch::with_alter(Step::G, 4, 1));
7325 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
7326 let mut stack = CommandStack::new(50);
7327 let command = Command::CycleEnharmonicSpelling(CycleEnharmonicSpellingCmd {
7328 part_index: 0,
7329 staff_index: 0,
7330 measure_index: 0,
7331 voice: 0,
7332 note_index: 0,
7333 pitch_index: Some(1),
7334 });
7335 let json = serde_json::to_string(&command).expect("command serializes");
7336 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7337 assert_eq!(command_key(&decoded), "CycleEnharmonicSpelling");
7338 stack.execute(decoded, &mut score).expect("spelling cycles");
7339 let pitches = |score: &Score| {
7340 score.parts[0].staves[0].measures[0].voices[0][0]
7341 .pitches
7342 .clone()
7343 };
7344 assert_eq!(
7345 pitches(&score),
7346 vec![
7347 Pitch::with_alter(Step::C, 4, 1),
7348 Pitch::with_alter(Step::A, 4, -1)
7349 ]
7350 );
7351 stack.undo(&mut score).expect("spelling undoes");
7352 assert_eq!(pitches(&score)[1], Pitch::with_alter(Step::G, 4, 1));
7353
7354 let mut rest_score = Score::new("Rest", 120, 4, 4, 0, 1);
7355 rest_score.parts[0].staves[0].measures[0].voices[0] = vec![Note::rest(Duration::Whole)];
7356 assert!(matches!(
7357 apply_command(&command, &mut rest_score),
7358 Err(Error::InvalidCommand(_))
7359 ));
7360 }
7361
7362 #[test]
7363 fn resequence_rehearsal_marks_continues_the_first_sequence() {
7364 let mut score = Score::new("Marks", 120, 4, 4, 0, 30);
7365 let mut stack = CommandStack::new(50);
7366 for (index, text) in [(0, "A"), (4, "C"), (9, "C"), (12, "Q")] {
7367 for_each_measure_at(&mut score, index, |measure| {
7368 measure.rehearsal = Some(text.into())
7369 });
7370 }
7371 let command = Command::ResequenceRehearsalMarks(ResequenceRehearsalMarksCmd::default());
7372 let json = serde_json::to_string(&command).expect("command serializes");
7373 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7374 assert_eq!(command_key(&decoded), "ResequenceRehearsalMarks");
7375 stack
7376 .execute(decoded, &mut score)
7377 .expect("marks resequence");
7378 let marks = |score: &Score| -> Vec<Option<String>> {
7379 [0, 4, 9, 12]
7380 .iter()
7381 .map(|&index| score.parts[0].staves[0].measures[index].rehearsal.clone())
7382 .collect()
7383 };
7384 assert_eq!(
7385 marks(&score),
7386 ["A", "B", "C", "D"].map(|text| Some(text.to_string()))
7387 );
7388 stack.undo(&mut score).expect("resequence undoes");
7389 assert_eq!(marks(&score)[1].as_deref(), Some("C"));
7390
7391 let sequence = RehearsalSequence::detect("Z", 1).expect("letters");
7392 assert_eq!(sequence.nth(1, 1), "AA");
7393 assert_eq!(sequence.nth(2, 1), "AB");
7394 let lower = RehearsalSequence::detect("y", 1).expect("lower letters");
7395 assert_eq!(lower.nth(2, 1), "aa");
7396 assert_eq!(
7397 RehearsalSequence::detect("7", 3)
7398 .expect("numbers")
7399 .nth(2, 9),
7400 "9"
7401 );
7402 assert_eq!(
7403 RehearsalSequence::detect("5", 5)
7404 .expect("measure numbers")
7405 .nth(3, 17),
7406 "17"
7407 );
7408 assert!(RehearsalSequence::detect("Intro", 1).is_none());
7409
7410 for_each_measure_at(&mut score, 0, |measure| {
7411 measure.rehearsal = Some("Intro".into())
7412 });
7413 assert!(matches!(
7414 apply_command(&command, &mut score),
7415 Err(Error::InvalidCommand(_))
7416 ));
7417 }
7418
7419 #[test]
7420 fn system_break_interval_replaces_line_breaks_only() {
7421 let mut score = Score::new("Breaks", 120, 4, 4, 0, 10);
7422 score.parts[0].staves[0].measures[1].system_break = true;
7423 score.parts[0].staves[0].measures[2].page_break = true;
7424 apply_command(
7425 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7426 interval: 4,
7427 start_measure: None,
7428 end_measure: None,
7429 }),
7430 &mut score,
7431 )
7432 .expect("breaks every four measures");
7433 let breaks: Vec<usize> = score.parts[0].staves[0]
7434 .measures
7435 .iter()
7436 .enumerate()
7437 .filter(|(_, measure)| measure.system_break)
7438 .map(|(index, _)| index)
7439 .collect();
7440 assert_eq!(breaks, vec![3, 7]);
7441 assert!(score.parts[0].staves[0].measures[2].page_break);
7442
7443 let mut exact = Score::new("Exact", 120, 4, 4, 0, 8);
7444 apply_command(
7445 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7446 interval: 4,
7447 start_measure: None,
7448 end_measure: None,
7449 }),
7450 &mut exact,
7451 )
7452 .expect("no break after the final measure");
7453 assert!(!exact.parts[0].staves[0].measures[7].system_break);
7454 assert!(exact.parts[0].staves[0].measures[3].system_break);
7455
7456 apply_command(
7457 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7458 interval: 0,
7459 start_measure: Some(0),
7460 end_measure: Some(6),
7461 }),
7462 &mut score,
7463 )
7464 .expect("breaks removed in range");
7465 assert!(!score.parts[0].staves[0].measures[3].system_break);
7466 assert!(score.parts[0].staves[0].measures[7].system_break);
7467 assert!(matches!(
7468 apply_command(
7469 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7470 interval: 2,
7471 start_measure: Some(4),
7472 end_measure: Some(11),
7473 }),
7474 &mut score,
7475 ),
7476 Err(Error::InvalidCommand(_))
7477 ));
7478 }
7479
7480 #[test]
7481 fn remove_trailing_empty_measures_keeps_content_and_final_barline() {
7482 let mut score = Score::new("Trailing", 120, 4, 4, 0, 6);
7483 let mut stack = CommandStack::new(50);
7484 score.parts[0].staves[0].measures[1].voices[0] =
7485 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
7486 score.parts[0].staves[0].measures[5].barline_right = Barline::Final;
7487 let mut fermata_rest = Note::rest(Duration::Whole);
7488 fermata_rest.articulations = vec![Articulation::Fermata];
7489 score.parts[0].staves[0].measures[3].voices[0] = vec![fermata_rest];
7490 stack
7491 .execute(
7492 Command::RemoveTrailingEmptyMeasures(RemoveTrailingEmptyMeasuresCmd {}),
7493 &mut score,
7494 )
7495 .expect("trailing measures removed");
7496 let staff = &score.parts[0].staves[0];
7497 assert_eq!(staff.measures.len(), 4, "a rest with a fermata is content");
7498 assert!(matches!(staff.measures[3].barline_right, Barline::Final));
7499 assert_eq!(staff.measures[3].number, 4);
7500 stack.undo(&mut score).expect("removal undoes");
7501 assert_eq!(score.parts[0].staves[0].measures.len(), 6);
7502
7503 let mut empty = Score::new("Empty", 120, 4, 4, 0, 3);
7504 apply_command(
7505 &Command::RemoveTrailingEmptyMeasures(RemoveTrailingEmptyMeasuresCmd {}),
7506 &mut empty,
7507 )
7508 .expect("empty score keeps one measure");
7509 assert_eq!(empty.parts[0].staves[0].measures.len(), 1);
7510 }
7511
7512 #[test]
7513 fn typed_spanner_commands_are_atomic_and_undoable() {
7514 use crate::model::score::{NotationSpanner, NotationSpannerKind};
7515
7516 let mut score = default_engine_score();
7517 let address = NoteAddr {
7518 part: 0,
7519 staff: 0,
7520 measure: 0,
7521 voice: 0,
7522 note: 0,
7523 };
7524 let spanner = NotationSpanner {
7525 id: "slur-1".to_string(),
7526 kind: NotationSpannerKind::Slur,
7527 start: address.clone(),
7528 end: address,
7529 number: Some(1),
7530 line_type: Some("dashed".to_string()),
7531 text: None,
7532 placement: Some("above".to_string()),
7533 ottava_size: None,
7534 ottava_type: None,
7535 };
7536 let mut stack = CommandStack::new(8);
7537 stack
7538 .execute(Command::AddSpanner(AddSpannerCmd { spanner }), &mut score)
7539 .unwrap();
7540 assert_eq!(score.spanners.len(), 1);
7541 assert_eq!(stack.undo_key().as_deref(), Some("AddSpanner"));
7542
7543 let before_duplicate = score.clone();
7544 let duplicate = score.spanners[0].clone();
7545 assert!(
7546 stack
7547 .execute(
7548 Command::AddSpanner(AddSpannerCmd { spanner: duplicate }),
7549 &mut score,
7550 )
7551 .is_err()
7552 );
7553 assert_eq!(score.spanners, before_duplicate.spanners);
7554
7555 let mut updated = score.spanners[0].clone();
7556 updated.number = Some(2);
7557 stack
7558 .execute(
7559 Command::UpdateSpanner(UpdateSpannerCmd { spanner: updated }),
7560 &mut score,
7561 )
7562 .unwrap();
7563 assert_eq!(score.spanners[0].number, Some(2));
7564 stack.undo(&mut score).unwrap();
7565 assert_eq!(score.spanners[0].number, Some(1));
7566 stack.redo(&mut score).unwrap();
7567 assert_eq!(score.spanners[0].number, Some(2));
7568
7569 stack
7570 .execute(
7571 Command::RemoveSpanner(RemoveSpannerCmd {
7572 id: "slur-1".to_string(),
7573 }),
7574 &mut score,
7575 )
7576 .unwrap();
7577 assert!(score.spanners.is_empty());
7578 stack.undo(&mut score).unwrap();
7579 assert_eq!(score.spanners.len(), 1);
7580 }
7581
7582 #[test]
7583 fn string_quartet_linked_views_roundtrip_and_undo_redo_as_one_contract() {
7584 let mut score = Score::template(ScoreTemplate::StringQuartet);
7585 let mut stack = CommandStack::new(16);
7586 for (part, id, name) in [
7587 (0, "violin-1", "Violin I"),
7588 (1, "violin-2", "Violin II"),
7589 (2, "viola", "Viola"),
7590 (3, "cello", "Cello"),
7591 ] {
7592 stack
7593 .execute(
7594 Command::UpsertScoreView(UpsertScoreViewCmd {
7595 view: ScoreView::linked_part(id, name, part),
7596 }),
7597 &mut score,
7598 )
7599 .expect("linked part view applies");
7600 }
7601 assert_eq!(score.parts.len(), 4);
7602 assert_eq!(score.views.len(), 4);
7603 for (part, view) in score.views.iter().enumerate() {
7604 assert_eq!(view.parts, vec![part]);
7605 assert_eq!(score.resolve_view(&view.id).unwrap().parts.len(), 1);
7606 }
7607
7608 let restored: Score =
7609 serde_json::from_str(&serde_json::to_string(&score).expect("quartet score serializes"))
7610 .expect("quartet score deserializes");
7611 assert_eq!(restored.views, score.views);
7612
7613 for _ in 0..4 {
7614 stack.undo(&mut score).expect("view undo applies");
7615 }
7616 assert!(score.views.is_empty());
7617 for _ in 0..4 {
7618 stack.redo(&mut score).expect("view redo applies");
7619 }
7620 assert_eq!(score.views, restored.views);
7621 }
7622
7623 #[test]
7624 fn linked_view_rejects_invalid_projection_and_keeps_the_source_intact() {
7625 use crate::model::score::{ViewStaffKindOverride, ViewStaffRef, ViewStyleProperty};
7626
7627 let mut score = Score::template(ScoreTemplate::StringQuartet);
7628 let original = score.clone();
7629 let original_parts = serde_json::to_value(&score.parts).unwrap();
7630 let base = ScoreView::linked_part("viola", "Viola", 2);
7631 let mut invalid = base.clone();
7632 invalid.parts.push(2);
7633 assert!(
7634 apply_command(
7635 &Command::UpsertScoreView(UpsertScoreViewCmd { view: invalid }),
7636 &mut score
7637 )
7638 .is_err()
7639 );
7640 let mut invalid = base.clone();
7641 invalid
7642 .layout
7643 .hidden_staves
7644 .push(ViewStaffRef { part: 1, staff: 0 });
7645 assert!(
7646 apply_command(
7647 &Command::UpsertScoreView(UpsertScoreViewCmd { view: invalid }),
7648 &mut score
7649 )
7650 .is_err()
7651 );
7652 let mut invalid = base.clone();
7653 invalid.staff_kind_overrides.push(ViewStaffKindOverride {
7654 staff: ViewStaffRef { part: 2, staff: 0 },
7655 kind: StaffKind::Tablature,
7656 });
7657 assert!(
7658 apply_command(
7659 &Command::UpsertScoreView(UpsertScoreViewCmd { view: invalid }),
7660 &mut score
7661 )
7662 .is_err()
7663 );
7664 let mut invalid = base.clone();
7665 invalid
7666 .layout
7667 .typed_style_overrides
7668 .push(ViewStyleOverride {
7669 property: ViewStyleProperty::StaffSpace,
7670 value: f32::NAN,
7671 });
7672 assert!(
7673 apply_command(
7674 &Command::UpsertScoreView(UpsertScoreViewCmd { view: invalid }),
7675 &mut score
7676 )
7677 .is_err()
7678 );
7679 assert_eq!(score.views, original.views);
7680 assert_eq!(serde_json::to_value(&score.parts).unwrap(), original_parts);
7681
7682 let mut view = base;
7683 view.parts = vec![2, 0];
7684 view.layout
7685 .hidden_staves
7686 .push(ViewStaffRef { part: 0, staff: 0 });
7687 view.layout.typed_style_overrides.push(ViewStyleOverride {
7688 property: ViewStyleProperty::StaffSpace,
7689 value: 1.25,
7690 });
7691 view.staff_kind_overrides.push(ViewStaffKindOverride {
7692 staff: ViewStaffRef { part: 2, staff: 0 },
7693 kind: StaffKind::Percussion,
7694 });
7695 let mut stack = CommandStack::new(4);
7696 stack
7697 .execute(
7698 Command::UpsertScoreView(UpsertScoreViewCmd { view }),
7699 &mut score,
7700 )
7701 .expect("valid linked projection applies");
7702 let projected = score.resolve_view("viola").expect("view resolves");
7703 assert_eq!(projected.parts.len(), 2);
7704 assert_eq!(projected.parts[0].name, original.parts[2].name);
7705 assert_eq!(projected.parts[1].name, original.parts[0].name);
7706 assert_eq!(
7707 projected.parts[0].staves[0].presentation.kind,
7708 StaffKind::Percussion
7709 );
7710 assert_eq!(projected.views[0].layout.hidden_staves[0].part, 1);
7711 assert_eq!(projected.views[0].layout.resolved_style().staff_space, 1.25);
7712 assert_eq!(serde_json::to_value(&score.parts).unwrap(), original_parts);
7713 stack.undo(&mut score).expect("view creation is undoable");
7714 assert!(score.views.is_empty());
7715 }
7716
7717 #[test]
7718 fn template_score_edit_session_keeps_global_and_measure_marks_in_sync() {
7719 let mut score = Score::default();
7720 apply_command(
7721 &Command::NewScore(NewScoreCmd {
7722 title: "Quartet rehearsal".into(),
7723 composer: "Composer".into(),
7724 tempo_bpm: 96,
7725 time_numerator: 3,
7726 time_denominator: 4,
7727 key_fifths: 2,
7728 measure_count: 2,
7729 template: Some(ScoreTemplate::StringQuartet),
7730 }),
7731 &mut score,
7732 )
7733 .unwrap();
7734 assert_eq!(score.parts.len(), 4);
7735 assert_eq!(score.parts[3].staves[0].measures.len(), 2);
7736 assert_eq!(score.metadata.composer, "Composer");
7737 for command in [
7738 Command::SetKeySignature(SetKeySignatureCmd { fifths: -1 }),
7739 Command::SetRehearsalMark(SetRehearsalMarkCmd {
7740 measure_index: 0,
7741 text: Some("A".into()),
7742 }),
7743 Command::SetNavigationMark(SetNavigationMarkCmd {
7744 measure_index: 1,
7745 mark: Some("Fine".into()),
7746 }),
7747 Command::SetSystemBreak(SetSystemBreakCmd {
7748 measure_index: 1,
7749 value: true,
7750 }),
7751 Command::SetPageBreak(SetPageBreakCmd {
7752 measure_index: 1,
7753 value: true,
7754 }),
7755 ] {
7756 apply_command(&command, &mut score).unwrap();
7757 }
7758 assert_eq!(score.settings.key_signature.fifths, -1);
7759 for part in &score.parts {
7760 let measures = &part.staves[0].measures;
7761 assert_eq!(measures[0].rehearsal.as_deref(), Some("A"));
7762 assert_eq!(measures[1].navigation.as_deref(), Some("Fine"));
7763 assert!(measures[1].system_break);
7764 assert!(measures[1].page_break);
7765 }
7766 }
7767
7768 #[test]
7769 fn section_break_is_atomic_undoable_and_synced_across_staves() {
7770 let mut score = Score::template(ScoreTemplate::Piano);
7771 let original = score.clone();
7772 let mut stack = CommandStack::new(4);
7773 stack
7774 .execute(
7775 Command::SetSectionBreak(SetSectionBreakCmd {
7776 measure_index: 2,
7777 value: true,
7778 }),
7779 &mut score,
7780 )
7781 .expect("section break applies");
7782 assert!(
7783 score
7784 .parts
7785 .iter()
7786 .flat_map(|part| part.staves.iter())
7787 .all(|staff| staff.measures[2].section_break)
7788 );
7789 stack.undo(&mut score).expect("section break undoes");
7790 assert_eq!(
7791 serde_json::to_value(&score).unwrap(),
7792 serde_json::to_value(&original).unwrap()
7793 );
7794
7795 assert!(
7796 stack
7797 .execute(
7798 Command::SetSectionBreak(SetSectionBreakCmd {
7799 measure_index: 99,
7800 value: true,
7801 }),
7802 &mut score,
7803 )
7804 .is_err()
7805 );
7806 assert_eq!(
7807 serde_json::to_value(&score).unwrap(),
7808 serde_json::to_value(&original).unwrap()
7809 );
7810 }
7811
7812 #[test]
7813 fn notation_editing_session_keeps_score_valid_and_undoes_atomically() {
7814 let mut score = Score::new("Notation session", 120, 4, 4, 0, 1);
7815 score.parts[0].staves[0].measures[0].voices[0] = (0..4)
7816 .map(|_| Note::new(Pitch::new(Step::C, 4), Duration::Quarter))
7817 .collect();
7818 let original = serde_json::to_value(&score).unwrap();
7819 let mut stack = CommandStack::new(16);
7820 let edits = [
7821 (
7822 Command::AddPitch(AddPitchCmd {
7823 part_index: 0,
7824 staff_index: 0,
7825 measure_index: 0,
7826 voice: 0,
7827 note_index: 0,
7828 pitch: Pitch::new(Step::E, 4),
7829 }),
7830 "Add Pitch",
7831 "AddPitch",
7832 ),
7833 (
7834 Command::SetArpeggio(SetArpeggioCmd {
7835 part_index: 0,
7836 staff_index: 0,
7837 measure_index: 0,
7838 voice_index: 0,
7839 note_index: 0,
7840 direction: Some(true),
7841 }),
7842 "Set Arpeggio",
7843 "SetArpeggio",
7844 ),
7845 (
7846 Command::SetStem(SetStemCmd {
7847 part_index: 0,
7848 staff_index: 0,
7849 measure_index: 0,
7850 voice_index: 0,
7851 note_index: 0,
7852 stem_up: Some(true),
7853 }),
7854 "Set Stem",
7855 "SetStem",
7856 ),
7857 (
7858 Command::SetDynamic(SetDynamicCmd {
7859 part_index: 0,
7860 staff_index: 0,
7861 measure_index: 0,
7862 voice: 0,
7863 note_index: 0,
7864 dynamic: Some(Dynamic::Mf),
7865 }),
7866 "Set Dynamic",
7867 "SetDynamic",
7868 ),
7869 (
7870 Command::ToggleArticulation(ToggleArticulationCmd {
7871 part_index: 0,
7872 staff_index: 0,
7873 measure_index: 0,
7874 voice: 0,
7875 note_index: 1,
7876 articulation: Articulation::Staccato,
7877 }),
7878 "Toggle Articulation",
7879 "ToggleArticulation",
7880 ),
7881 (
7882 Command::ToggleTie(ToggleTieCmd {
7883 part_index: 0,
7884 staff_index: 0,
7885 measure_index: 0,
7886 voice: 0,
7887 note_index: 1,
7888 }),
7889 "Toggle Tie",
7890 "ToggleTie",
7891 ),
7892 (
7893 Command::AddHairpin(AddHairpinCmd {
7894 part_index: 0,
7895 staff_index: 0,
7896 measure_index: 0,
7897 voice: 0,
7898 start_note_idx: 0,
7899 end_note_idx: 3,
7900 kind: HairpinKind::Crescendo,
7901 }),
7902 "Add Hairpin",
7903 "AddHairpin",
7904 ),
7905 (
7906 Command::AddPedal(AddPedalCmd {
7907 part_index: 0,
7908 staff_index: 0,
7909 measure_index: 0,
7910 voice: 0,
7911 start_note_idx: 0,
7912 end_note_idx: 3,
7913 }),
7914 "Add Pedal",
7915 "AddPedal",
7916 ),
7917 (
7918 Command::SetBarline(SetBarlineCmd {
7919 measure_index: 0,
7920 side: "right".into(),
7921 barline: Barline::Double,
7922 }),
7923 "Set Barline",
7924 "SetBarline",
7925 ),
7926 ];
7927 for (command, label, key) in edits {
7928 stack.execute(command, &mut score).unwrap();
7929 assert_eq!(stack.undo_label().as_deref(), Some(label));
7930 assert_eq!(stack.undo_key().as_deref(), Some(key));
7931 }
7932 assert!(validate(&score).is_valid());
7933 let voice = &score.parts[0].staves[0].measures[0].voices[0];
7934 assert_eq!(voice[0].pitches.len(), 2);
7935 assert_eq!(voice[0].arpeggiate, Some(true));
7936 assert_eq!(voice[0].hairpin_start, Some(HairpinKind::Crescendo));
7937 assert!(voice[0].pedal_start);
7938 assert!(voice[1].tie_start && voice[2].tie_end);
7939 assert!(voice[3].pedal_end && voice[3].hairpin_end);
7940 assert_eq!(
7941 score.parts[0].staves[0].measures[0].barline_right,
7942 Barline::Double
7943 );
7944 let edited = serde_json::to_value(&score).unwrap();
7945 for _ in 0..9 {
7946 stack.undo(&mut score).unwrap();
7947 }
7948 assert_eq!(serde_json::to_value(&score).unwrap(), original);
7949 for _ in 0..9 {
7950 stack.redo(&mut score).unwrap();
7951 }
7952 assert_eq!(serde_json::to_value(&score).unwrap(), edited);
7953 }
7954
7955 #[test]
7956 fn guitar_annotation_commands_keep_json_and_score_semantics() {
7957 let mut score = Score::new("Guitar annotations", 120, 4, 4, 0, 1);
7958 score.parts[0].staves[0].measures[0].voices[0] = (0..4)
7959 .map(|_| Note::new(Pitch::new(Step::C, 4), Duration::Quarter))
7960 .collect();
7961 let edits = [
7962 (
7963 serde_json::json!({"type":"set_tablature_config", "part_index":0,
7964 "staff_index":0, "config":{"lines":6,"tuning_midi":[40,45,50,55,59,64],"capo":0}}),
7965 "SetTablatureConfig",
7966 ),
7967 (
7968 serde_json::json!({"type":"set_fingering", "part_index":0,"staff_index":0,
7969 "measure_index":0,"voice":0,"note_index":0,"fingering":2}),
7970 "SetFingering",
7971 ),
7972 (
7973 serde_json::json!({"type":"set_fingerings", "part_index":0,"staff_index":0,
7974 "measure_index":0,"voice":0,"note_index":0,"fingerings":[1,3]}),
7975 "SetFingerings",
7976 ),
7977 (
7978 serde_json::json!({"type":"set_string_number", "part_index":0,"staff_index":0,
7979 "measure_index":0,"voice":0,"note_index":0,"string_number":2}),
7980 "SetStringNumber",
7981 ),
7982 (
7983 serde_json::json!({"type":"set_tab_position", "part_index":0,"staff_index":0,
7984 "measure_index":0,"voice":0,"note_index":0,"position":{"string":2,"fret":1}}),
7985 "SetTabPosition",
7986 ),
7987 (
7988 serde_json::json!({"type":"set_guitar_technique", "part_index":0,"staff_index":0,
7989 "measure_index":0,"voice":0,"note_index":0,"technique":"slide"}),
7990 "SetGuitarTechnique",
7991 ),
7992 (
7993 serde_json::json!({"type":"set_technique_text", "part_index":0,"staff_index":0,
7994 "measure_index":0,"voice":0,"note_index":0,"text":"let ring"}),
7995 "SetTechniqueText",
7996 ),
7997 (
7998 serde_json::json!({"type":"set_note_head", "part_index":0,"staff_index":0,
7999 "measure_index":0,"voice":0,"note_index":0,"note_head":"Diamond"}),
8000 "SetNoteHead",
8001 ),
8002 (
8003 serde_json::json!({"type":"set_hidden", "part_index":0,"staff_index":0,
8004 "measure_index":0,"voice":0,"note_index":0,"hidden":true}),
8005 "SetHidden",
8006 ),
8007 (
8008 serde_json::json!({"type":"set_instrument_id", "part_index":0,"staff_index":0,
8009 "measure_index":0,"voice":0,"note_index":0,"instrument_id":"guitar"}),
8010 "SetInstrumentId",
8011 ),
8012 (
8013 serde_json::json!({"type":"set_note_placement", "part_index":0,"staff_index":0,
8014 "measure_index":0,"voice":0,"note_index":0,"offset_x":1.5,"offset_y":-2.0}),
8015 "SetNotePlacement",
8016 ),
8017 (
8018 serde_json::json!({"type":"set_expression_text", "measure_index":0,
8019 "text":"dolce"}),
8020 "SetExpressionText",
8021 ),
8022 ];
8023 for (value, key) in edits {
8024 let command: Command = serde_json::from_value(value).unwrap();
8025 assert_eq!(command_key(&command), key);
8026 assert!(!command_label(&command).is_empty());
8027 let hint = command_hint(&command);
8028 match key {
8029 "SetTablatureConfig" => assert_eq!(hint.scope, ChangeScope::Part(0)),
8030 "SetExpressionText" => assert_eq!(hint.scope, ChangeScope::Global),
8031 _ => assert_eq!(
8032 hint.scope,
8033 ChangeScope::Measures {
8034 part: 0,
8035 staff: 0,
8036 start: 0,
8037 end: 1
8038 }
8039 ),
8040 }
8041 apply_command(&command, &mut score).unwrap();
8042 }
8043 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
8044 assert_eq!(note.fingerings, vec![1, 3]);
8045 assert_eq!(
8046 note.tab_position,
8047 Some(crate::TabPosition { string: 2, fret: 1 })
8048 );
8049 assert_eq!(note.guitar_technique, Some(GuitarTechnique::Slide));
8050 assert_eq!(note.technique_text.as_deref(), Some("let ring"));
8051 assert_eq!(note.note_head, NoteHead::Diamond);
8052 assert!(note.hidden);
8053 assert_eq!(note.instrument_id.as_deref(), Some("guitar"));
8054 assert_eq!(note.offset_x, Some(1.5));
8055 assert_eq!(
8056 score.parts[0].staves[0].measures[0]
8057 .expression_text
8058 .as_deref(),
8059 Some("dolce")
8060 );
8061 assert!(validate(&score).is_valid());
8062 }
8063
8064 #[test]
8065 fn note_edits_reject_invalid_values_without_changing_the_score() {
8066 let mut score = Score::new("Note edits", 120, 4, 4, 0, 1);
8067 score.parts[0].staves[0].measures[0].voices[0] = vec![
8068 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
8069 Note::rest(Duration::Quarter),
8070 ];
8071 let grace = |note_index| {
8072 Command::SetGrace(SetGraceCmd {
8073 part_index: 0,
8074 staff_index: 0,
8075 measure_index: 0,
8076 voice: 0,
8077 note_index,
8078 is_grace: true,
8079 slash: true,
8080 })
8081 };
8082 let before = serde_json::to_value(&score).unwrap();
8083 assert!(apply_command(&grace(1), &mut score).is_err());
8084 assert_eq!(serde_json::to_value(&score).unwrap(), before);
8085 apply_command(&grace(0), &mut score).unwrap();
8086 assert!(score.parts[0].staves[0].measures[0].voices[0][0].is_grace);
8087
8088 let placement = |offset_x| {
8089 Command::SetNotePlacement(SetNotePlacementCmd {
8090 part_index: 0,
8091 staff_index: 0,
8092 measure_index: 0,
8093 voice: 0,
8094 note_index: 0,
8095 offset_x,
8096 offset_y: None,
8097 relative_x: None,
8098 relative_y: None,
8099 })
8100 };
8101 let before = serde_json::to_value(&score).unwrap();
8102 assert!(apply_command(&placement(Some(f64::NAN)), &mut score).is_err());
8103 assert_eq!(serde_json::to_value(&score).unwrap(), before);
8104 apply_command(&placement(Some(1.5)), &mut score).unwrap();
8105 assert_eq!(
8106 score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
8107 Some(1.5)
8108 );
8109 apply_command(&placement(None), &mut score).unwrap();
8110 assert_eq!(
8111 score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
8112 None
8113 );
8114
8115 let cross_staff = |target_staff| {
8116 Command::SetCrossStaff(SetCrossStaffCmd {
8117 part_index: 0,
8118 staff_index: 0,
8119 measure_index: 0,
8120 voice: 0,
8121 note_index: 0,
8122 placement: Some(CrossStaff {
8123 target_staff,
8124 target_voice: None,
8125 }),
8126 })
8127 };
8128 let before = serde_json::to_value(&score).unwrap();
8129 assert!(apply_command(&cross_staff(0), &mut score).is_err());
8130 assert!(apply_command(&cross_staff(2), &mut score).is_err());
8131 assert_eq!(serde_json::to_value(&score).unwrap(), before);
8132 }
8133
8134 #[test]
8135 fn editor_command_metadata_survives_json_roundtrip() {
8136 let commands = [
8137 (
8138 serde_json::json!({"type":"set_tempo", "bpm":96}),
8139 "SetTempo",
8140 ),
8141 (
8142 serde_json::json!({"type":"add_measure", "after_index":0}),
8143 "AddMeasure",
8144 ),
8145 (
8146 serde_json::json!({"type":"delete_measure", "measure_index":0}),
8147 "DeleteMeasure",
8148 ),
8149 (
8150 serde_json::json!({"type":"set_key_signature", "fifths":2}),
8151 "SetKeySignature",
8152 ),
8153 (
8154 serde_json::json!({"type":"set_time_signature", "numerator":3,"denominator":4}),
8155 "SetTimeSignature",
8156 ),
8157 (
8158 serde_json::json!({"type":"set_rehearsal_mark", "measure_index":0,"text":"A"}),
8159 "SetRehearsalMark",
8160 ),
8161 (
8162 serde_json::json!({"type":"set_navigation_mark", "measure_index":0,"mark":"Coda"}),
8163 "SetNavigationMark",
8164 ),
8165 (
8166 serde_json::json!({"type":"set_figured_bass", "measure_index":0,"figures":[]}),
8167 "SetFiguredBass",
8168 ),
8169 (
8170 serde_json::json!({"type":"set_multi_rest", "measure_index":0,"count":2}),
8171 "SetMultiRest",
8172 ),
8173 (
8174 serde_json::json!({"type":"set_volta", "measure_index":0,"volta":null}),
8175 "SetVolta",
8176 ),
8177 (
8178 serde_json::json!({"type":"set_clef", "part_index":0,"staff_index":0,"clef":"Treble"}),
8179 "SetClef",
8180 ),
8181 (
8182 serde_json::json!({"type":"set_part_name", "part_index":0,"name":"Piano","short_name":"Pno."}),
8183 "SetPartName",
8184 ),
8185 (
8186 serde_json::json!({"type":"set_midi_instrument", "part_index":0,"midi_channel":0,"midi_program":1}),
8187 "SetMidiInstrument",
8188 ),
8189 (
8190 serde_json::json!({"type":"set_percussion_kit", "part_index":0,"instruments":[]}),
8191 "SetPercussionKit",
8192 ),
8193 (
8194 serde_json::json!({"type":"set_instrument_definition", "part_index":0,"definition":null}),
8195 "SetInstrumentDefinition",
8196 ),
8197 (
8198 serde_json::json!({"type":"set_measure_instrument_change", "part_index":0,"staff_index":0,"measure_index":0,"definition":null}),
8199 "SetMeasureInstrumentChange",
8200 ),
8201 (
8202 serde_json::json!({"type":"set_measure_tablature_change", "part_index":0,"staff_index":0,"measure_index":0,"config":null}),
8203 "SetMeasureTablatureChange",
8204 ),
8205 (
8206 serde_json::json!({"type":"set_mid_measure_clefs", "part_index":0,"staff_index":0,"measure_index":0,"clefs":[]}),
8207 "SetMidMeasureClefs",
8208 ),
8209 (
8210 serde_json::json!({"type":"remove_score_view", "id":"part"}),
8211 "RemoveScoreView",
8212 ),
8213 (
8214 serde_json::json!({"type":"set_transpose", "part_index":0,"staff_index":0,"semitones":-2}),
8215 "SetTranspose",
8216 ),
8217 (
8218 serde_json::json!({"type":"set_tempo_at_measure", "measure_index":0,"bpm":100}),
8219 "SetTempoAtMeasure",
8220 ),
8221 (
8222 serde_json::json!({"type":"set_tempo_ramp_at_measure", "measure_index":0,"target_bpm":120}),
8223 "SetTempoRampAtMeasure",
8224 ),
8225 (
8226 serde_json::json!({"type":"set_system_break", "measure_index":0,"value":true}),
8227 "SetSystemBreak",
8228 ),
8229 (
8230 serde_json::json!({"type":"set_page_break", "measure_index":0,"value":true}),
8231 "SetPageBreak",
8232 ),
8233 (
8234 serde_json::json!({"type":"set_section_break", "measure_index":0,"value":true}),
8235 "SetSectionBreak",
8236 ),
8237 (
8238 serde_json::json!({"type":"set_part_group", "group":null}),
8239 "SetPartGroup",
8240 ),
8241 (
8242 serde_json::json!({"type":"remove_spanner", "id":"s1"}),
8243 "RemoveSpanner",
8244 ),
8245 (
8246 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}),
8247 "SetGrace",
8248 ),
8249 (
8250 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}),
8251 "SetOttava",
8252 ),
8253 (
8254 serde_json::json!({"type":"set_lyric", "part_index":0,"staff_index":0,"measure_index":0,"voice":0,"note_index":0,"lyric":null}),
8255 "SetLyric",
8256 ),
8257 (
8258 serde_json::json!({"type":"set_chord_symbol", "part_index":0,"staff_index":0,"measure_index":0,"voice":0,"note_index":0,"chord":null}),
8259 "SetChordSymbol",
8260 ),
8261 (
8262 serde_json::json!({"type":"set_harmony_range", "part_index":0,"staff_index":0,"measure_index":0,"voice":0,"note_index":0,"end":null}),
8263 "SetHarmonyRange",
8264 ),
8265 (
8266 serde_json::json!({"type":"set_harp_pedal_diagrams", "part_index":0,"staff_index":0,"measure_index":0,"diagrams":[]}),
8267 "SetHarpPedalDiagrams",
8268 ),
8269 (
8270 serde_json::json!({"type":"paste_voice", "part_index":0,"staff_index":0,"measure_index":0,"voice_index":0,"notes":[]}),
8271 "PasteVoice",
8272 ),
8273 (
8274 serde_json::json!({"type":"paste_range", "part_index":0,"staff_index":0,"voice_index":0,"target_measure":0,"measures":[]}),
8275 "PasteRange",
8276 ),
8277 (
8278 serde_json::json!({"type":"exchange_voices", "part_index":0,"staff_index":0,"start_measure":0,"end_measure":0,"first_voice":0,"second_voice":1}),
8279 "ExchangeVoices",
8280 ),
8281 (
8282 serde_json::json!({"type":"split_measure", "measure_index":0,"split_at_beats":2.0}),
8283 "SplitMeasure",
8284 ),
8285 (
8286 serde_json::json!({"type":"join_measures", "measure_index":0}),
8287 "JoinMeasures",
8288 ),
8289 (
8290 serde_json::json!({"type":"implode_staves", "part_index":0,"source_staves":[0,1],"target_staff":0,"start_measure":0,"end_measure":0}),
8291 "ImplodeStaves",
8292 ),
8293 (
8294 serde_json::json!({"type":"explode_voices", "part_index":0,"source_staff":0,"target_staves":[0,1],"start_measure":0,"end_measure":0}),
8295 "ExplodeVoices",
8296 ),
8297 (
8298 serde_json::json!({"type":"explode_chord_pitches", "part_index":0,"source_staff":0,"target_staves":[0,1],"start_measure":0,"end_measure":0}),
8299 "ExplodeChordPitches",
8300 ),
8301 (
8302 serde_json::json!({"type":"scale_voice_range", "part_index":0,"staff_index":0,"voice":0,"start_measure":0,"end_measure":0,"scale":"half"}),
8303 "ScaleVoiceRange",
8304 ),
8305 (
8306 serde_json::json!({"type":"add_staff", "part_index":0,"clef":"Bass"}),
8307 "AddStaff",
8308 ),
8309 (
8310 serde_json::json!({"type":"delete_staff", "part_index":0,"staff_index":1}),
8311 "DeleteStaff",
8312 ),
8313 (
8314 serde_json::json!({"type":"set_tuplet", "part_index":0,"staff_index":0,"measure_index":0,"voice_index":0,"note_index":0,"tuplet":null}),
8315 "SetTuplet",
8316 ),
8317 (
8318 serde_json::json!({"type":"set_stem", "part_index":0,"staff_index":0,"measure_index":0,"voice_index":0,"note_index":0,"stem_up":true}),
8319 "SetStem",
8320 ),
8321 ];
8322 for (value, expected_key) in commands {
8323 let command: Command = serde_json::from_value(value).unwrap();
8324 let restored: Command =
8325 serde_json::from_slice(&serde_json::to_vec(&command).unwrap()).unwrap();
8326 assert_eq!(command_key(&restored), expected_key);
8327 assert!(!command_label(&restored).is_empty(), "{expected_key}");
8328 let before = command_hint(&command);
8329 let after = command_hint(&restored);
8330 assert_eq!(after.scope, before.scope);
8331 assert_eq!(after.layout_dirty, before.layout_dirty);
8332 assert_eq!(after.playback_dirty, before.playback_dirty);
8333 }
8334 }
8335}