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_end = false;
3147 }
3148 voice[cmd.start_note_idx].hairpin_start = Some(cmd.kind);
3149 voice[cmd.end_note_idx].hairpin_end = true;
3150 Ok(())
3151}
3152
3153fn apply_add_pedal(cmd: &AddPedalCmd, score: &mut Score) -> Result<(), Error> {
3154 let voice = score
3155 .parts
3156 .get_mut(cmd.part_index)
3157 .ok_or(Error::PartNotFound(cmd.part_index))?
3158 .staves
3159 .get_mut(cmd.staff_index)
3160 .ok_or(Error::StaffNotFound(cmd.staff_index))?
3161 .measures
3162 .get_mut(cmd.measure_index)
3163 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
3164 .voices
3165 .get_mut(cmd.voice)
3166 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
3167 if cmd.start_note_idx >= voice.len() {
3168 return Err(Error::NoteNotFound(cmd.start_note_idx));
3169 }
3170 if cmd.end_note_idx >= voice.len() {
3171 return Err(Error::NoteNotFound(cmd.end_note_idx));
3172 }
3173 if cmd.start_note_idx >= cmd.end_note_idx {
3174 return Err(Error::InvalidCommand(
3175 "start_note_idx must be less than end_note_idx".into(),
3176 ));
3177 }
3178 for note in voice
3179 .iter_mut()
3180 .take(cmd.end_note_idx + 1)
3181 .skip(cmd.start_note_idx)
3182 {
3183 note.pedal_start = false;
3184 note.pedal_end = false;
3185 }
3186 voice[cmd.start_note_idx].pedal_start = true;
3187 voice[cmd.end_note_idx].pedal_end = true;
3188 Ok(())
3189}
3190
3191fn apply_toggle_tie(cmd: &ToggleTieCmd, score: &mut Score) -> Result<(), Error> {
3192 let current_tie_start = {
3193 let v = score
3194 .parts
3195 .get(cmd.part_index)
3196 .ok_or(Error::PartNotFound(cmd.part_index))?
3197 .staves
3198 .get(cmd.staff_index)
3199 .ok_or(Error::StaffNotFound(cmd.staff_index))?
3200 .measures
3201 .get(cmd.measure_index)
3202 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
3203 .voices
3204 .get(cmd.voice)
3205 .ok_or(Error::VoiceOutOfRange(cmd.voice))?;
3206 v.get(cmd.note_index)
3207 .ok_or(Error::NoteNotFound(cmd.note_index))?
3208 .tie_start
3209 };
3210 let voice_len = score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index]
3211 .voices[cmd.voice]
3212 .len();
3213 let total_measures = score.parts[cmd.part_index].staves[cmd.staff_index]
3214 .measures
3215 .len();
3216
3217 let new_tie = !current_tie_start;
3218 score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index].voices
3219 [cmd.voice][cmd.note_index]
3220 .tie_start = new_tie;
3221
3222 if cmd.note_index + 1 < voice_len {
3223 score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index].voices
3224 [cmd.voice][cmd.note_index + 1]
3225 .tie_end = new_tie;
3226 } else {
3227 let next_mi = cmd.measure_index + 1;
3228 if next_mi < total_measures {
3229 let next_voice = &mut score.parts[cmd.part_index].staves[cmd.staff_index].measures
3230 [next_mi]
3231 .voices[cmd.voice];
3232 if let Some(n) = next_voice.get_mut(0) {
3233 n.tie_end = new_tie;
3234 }
3235 }
3236 }
3237 Ok(())
3238}
3239
3240fn apply_set_dynamic(cmd: &SetDynamicCmd, score: &mut Score) -> Result<(), Error> {
3241 get_note_mut(
3242 score,
3243 cmd.part_index,
3244 cmd.staff_index,
3245 cmd.measure_index,
3246 cmd.voice,
3247 cmd.note_index,
3248 )?
3249 .dynamic = cmd.dynamic;
3250 Ok(())
3251}
3252
3253fn apply_toggle_articulation(cmd: &ToggleArticulationCmd, score: &mut Score) -> Result<(), Error> {
3254 let note = get_note_mut(
3255 score,
3256 cmd.part_index,
3257 cmd.staff_index,
3258 cmd.measure_index,
3259 cmd.voice,
3260 cmd.note_index,
3261 )?;
3262 if let Some(pos) = note
3263 .articulations
3264 .iter()
3265 .position(|a| a == &cmd.articulation)
3266 {
3267 note.articulations.remove(pos);
3268 } else {
3269 note.articulations.push(cmd.articulation.clone());
3270 }
3271 Ok(())
3272}
3273
3274fn apply_set_time_signature(cmd: &SetTimeSignatureCmd, score: &mut Score) -> Result<(), Error> {
3275 if cmd.numerator == 0 || cmd.denominator == 0 {
3276 return Err(Error::InvalidCommand(
3277 "time signature numerator and denominator must be > 0".into(),
3278 ));
3279 }
3280 if ![1u8, 2, 4, 8, 16, 32].contains(&cmd.denominator) {
3281 return Err(Error::InvalidCommand(format!(
3282 "invalid time signature denominator: {}",
3283 cmd.denominator
3284 )));
3285 }
3286 score.settings.time_signature = TimeSignature {
3287 numerator: cmd.numerator,
3288 denominator: cmd.denominator,
3289 };
3290 let max_beats = score.settings.time_signature.total_beats();
3291 for part in &mut score.parts {
3292 for staff in &mut part.staves {
3293 for measure in &mut staff.measures {
3294 let beats = measure
3296 .actual_length
3297 .and_then(|length| length.beats())
3298 .unwrap_or(max_beats);
3299 for voice in &mut measure.voices {
3300 trim_voice_to_measure(voice, beats);
3301 pad_voice_to_measure(voice, beats);
3302 }
3303 }
3304 }
3305 }
3306 Ok(())
3307}
3308
3309fn apply_set_barline(cmd: &SetBarlineCmd, score: &mut Score) -> Result<(), Error> {
3310 for part in &mut score.parts {
3311 for staff in &mut part.staves {
3312 let measure = staff
3313 .measures
3314 .get_mut(cmd.measure_index)
3315 .ok_or(Error::MeasureNotFound(cmd.measure_index))?;
3316 match cmd.side.as_str() {
3317 "left" => measure.barline_left = cmd.barline.clone(),
3318 "right" => measure.barline_right = cmd.barline.clone(),
3319 _ => {
3320 return Err(Error::InvalidCommand(format!(
3321 "invalid barline side: '{}'",
3322 cmd.side
3323 )));
3324 }
3325 }
3326 }
3327 }
3328 Ok(())
3329}
3330
3331fn apply_add_part(cmd: &AddPartCmd, score: &mut Score) -> Result<(), Error> {
3332 if cmd.clefs.is_empty() {
3333 return Err(Error::InvalidCommand(
3334 "AddPart requires at least one clef".into(),
3335 ));
3336 }
3337 let measure_count = score.measure_count();
3338 let ts = score.settings.time_signature.clone();
3339 let mut part = Part::new(&cmd.name, &cmd.short_name);
3340 part.midi_channel = cmd.midi_channel.min(15);
3341 part.midi_program = cmd.midi_program;
3342 for clef_str in &cmd.clefs {
3343 let clef = Clef::from_name(clef_str).unwrap_or(Clef::Treble);
3344 let mut staff = Staff::new(clef);
3345 for i in 0..measure_count {
3346 let mut m = Measure::empty(ts.numerator, ts.denominator);
3347 m.number = i as u32 + 1;
3348 staff.measures.push(m);
3349 }
3350 part.staves.push(staff);
3351 }
3352 score.parts.push(part);
3353 Ok(())
3354}
3355
3356fn apply_set_clef(cmd: &SetClefCmd, score: &mut Score) -> Result<(), Error> {
3357 score
3358 .parts
3359 .get_mut(cmd.part_index)
3360 .ok_or(Error::PartNotFound(cmd.part_index))?
3361 .staves
3362 .get_mut(cmd.staff_index)
3363 .ok_or(Error::StaffNotFound(cmd.staff_index))?
3364 .clef = cmd.clef.clone();
3365 Ok(())
3366}
3367
3368fn apply_set_part_name(cmd: &SetPartNameCmd, score: &mut Score) -> Result<(), Error> {
3369 let part = score
3370 .parts
3371 .get_mut(cmd.part_index)
3372 .ok_or(Error::PartNotFound(cmd.part_index))?;
3373 part.name = cmd.name.clone();
3374 part.short_name = cmd.short_name.clone();
3375 Ok(())
3376}
3377
3378fn apply_delete_part(cmd: &DeletePartCmd, score: &mut Score) -> Result<(), Error> {
3379 if cmd.part_index >= score.parts.len() {
3380 return Err(Error::PartNotFound(cmd.part_index));
3381 }
3382 score.parts.remove(cmd.part_index);
3383 remap_spanners(score, |address| {
3384 if address.part == cmd.part_index {
3385 None
3386 } else if address.part > cmd.part_index {
3387 let mut shifted = address.clone();
3388 shifted.part -= 1;
3389 Some(shifted)
3390 } else {
3391 Some(address.clone())
3392 }
3393 });
3394 Ok(())
3395}
3396
3397fn apply_reorder_parts(cmd: &ReorderPartsCmd, score: &mut Score) -> Result<(), Error> {
3398 let count = score.parts.len();
3399 if cmd.order.len() != count {
3400 return Err(Error::InvalidCommand(format!(
3401 "part order must contain exactly {count} entries"
3402 )));
3403 }
3404 let mut old_to_new = vec![usize::MAX; count];
3405 for (new_index, &old_index) in cmd.order.iter().enumerate() {
3406 if old_index >= count {
3407 return Err(Error::PartNotFound(old_index));
3408 }
3409 if std::mem::replace(&mut old_to_new[old_index], new_index) != usize::MAX {
3410 return Err(Error::InvalidCommand(format!(
3411 "part order contains duplicate index {old_index}"
3412 )));
3413 }
3414 }
3415 for view in &score.views {
3416 for &part in &view.parts {
3417 if part >= count {
3418 return Err(Error::PartNotFound(part));
3419 }
3420 }
3421 for override_ in &view.staff_kind_overrides {
3422 if override_.staff.part >= count {
3423 return Err(Error::PartNotFound(override_.staff.part));
3424 }
3425 }
3426 }
3427 for spanner in &score.spanners {
3428 for address in [&spanner.start, &spanner.end] {
3429 if address.part >= count {
3430 return Err(Error::PartNotFound(address.part));
3431 }
3432 }
3433 }
3434 for group in &score.part_groups {
3435 if group.first_part > group.last_part || group.last_part >= count {
3436 return Err(Error::InvalidCommand(
3437 "part group references an invalid part range".into(),
3438 ));
3439 }
3440 let mut remapped: Vec<_> = (group.first_part..=group.last_part)
3441 .map(|part| old_to_new[part])
3442 .collect();
3443 remapped.sort_unstable();
3444 if remapped
3445 .windows(2)
3446 .any(|pair| pair[1] != pair[0].saturating_add(1))
3447 {
3448 return Err(Error::InvalidCommand(
3449 "part order would split an existing part group".into(),
3450 ));
3451 }
3452 }
3453
3454 let existing = std::mem::take(&mut score.parts);
3455 score.parts = cmd
3456 .order
3457 .iter()
3458 .map(|&old_index| existing[old_index].clone())
3459 .collect();
3460 for view in &mut score.views {
3461 for part in &mut view.parts {
3462 *part = old_to_new[*part];
3463 }
3464 for override_ in &mut view.staff_kind_overrides {
3465 override_.staff.part = old_to_new[override_.staff.part];
3466 }
3467 }
3468 for group in &mut score.part_groups {
3469 group.first_part = old_to_new[group.first_part];
3470 group.last_part = old_to_new[group.last_part];
3471 if group.first_part > group.last_part {
3472 std::mem::swap(&mut group.first_part, &mut group.last_part);
3473 }
3474 }
3475 remap_spanners(score, |address| {
3476 let mut remapped = address.clone();
3477 remapped.part = old_to_new[address.part];
3478 Some(remapped)
3479 });
3480 Ok(())
3481}
3482
3483fn apply_set_metadata(cmd: &SetMetadataCmd, score: &mut Score) -> Result<(), Error> {
3484 if let Some(v) = &cmd.title {
3485 score.metadata.title = v.clone();
3486 }
3487 if let Some(v) = &cmd.composer {
3488 score.metadata.composer = v.clone();
3489 }
3490 if let Some(v) = &cmd.lyricist {
3491 score.metadata.lyricist = v.clone();
3492 }
3493 if let Some(v) = &cmd.copyright {
3494 score.metadata.copyright = v.clone();
3495 }
3496 if let Some(v) = &cmd.work_number {
3497 score.metadata.work_number = v.clone();
3498 }
3499 if let Some(v) = &cmd.movement_title {
3500 score.metadata.movement_title = v.clone();
3501 }
3502 Ok(())
3503}
3504
3505fn apply_set_midi_instrument(cmd: &SetMidiInstrumentCmd, score: &mut Score) -> Result<(), Error> {
3506 let part = score
3507 .parts
3508 .get_mut(cmd.part_index)
3509 .ok_or(Error::PartNotFound(cmd.part_index))?;
3510 part.midi_channel = cmd.midi_channel.min(15);
3511 part.midi_program = cmd.midi_program;
3512 Ok(())
3513}
3514
3515fn apply_set_percussion_kit(cmd: &SetPercussionKitCmd, score: &mut Score) -> Result<(), Error> {
3516 validate_percussion_kit(&cmd.instruments)?;
3517 let part = score
3518 .parts
3519 .get_mut(cmd.part_index)
3520 .ok_or(Error::PartNotFound(cmd.part_index))?;
3521 part.percussion_instruments = cmd.instruments.clone();
3522 Ok(())
3523}
3524
3525fn apply_set_instrument_definition(
3526 cmd: &SetInstrumentDefinitionCmd,
3527 score: &mut Score,
3528) -> Result<(), Error> {
3529 if let Some(definition) = &cmd.definition {
3530 validate_instrument_definition(definition)?;
3531 }
3532 let part = score
3533 .parts
3534 .get_mut(cmd.part_index)
3535 .ok_or(Error::PartNotFound(cmd.part_index))?;
3536 part.instrument = cmd.definition.clone();
3537 Ok(())
3538}
3539
3540fn apply_set_measure_instrument_change(
3541 cmd: &SetMeasureInstrumentChangeCmd,
3542 score: &mut Score,
3543) -> Result<(), Error> {
3544 if let Some(definition) = &cmd.definition {
3545 validate_instrument_definition(definition)?;
3546 }
3547 score
3548 .parts
3549 .get_mut(cmd.part_index)
3550 .ok_or(Error::PartNotFound(cmd.part_index))?
3551 .staves
3552 .get_mut(cmd.staff_index)
3553 .ok_or(Error::StaffNotFound(cmd.staff_index))?
3554 .measures
3555 .get_mut(cmd.measure_index)
3556 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
3557 .instrument_change = cmd.definition.clone();
3558 Ok(())
3559}
3560
3561fn apply_set_measure_tablature_change(
3562 cmd: &SetMeasureTablatureChangeCmd,
3563 score: &mut Score,
3564) -> Result<(), Error> {
3565 let staff = score
3566 .parts
3567 .get_mut(cmd.part_index)
3568 .ok_or(Error::PartNotFound(cmd.part_index))?
3569 .staves
3570 .get_mut(cmd.staff_index)
3571 .ok_or(Error::StaffNotFound(cmd.staff_index))?;
3572 let base = staff.tablature.as_ref().ok_or_else(|| {
3573 Error::InvalidCommand("tablature change requires a staff tablature configuration".into())
3574 })?;
3575 if let Some(config) = &cmd.config {
3576 validate_tablature_config(config)?;
3577 if config.lines != base.lines {
3578 return Err(Error::InvalidCommand(format!(
3579 "tablature change line count {} must match staff line count {}",
3580 config.lines, base.lines
3581 )));
3582 }
3583 }
3584 staff
3585 .measures
3586 .get_mut(cmd.measure_index)
3587 .ok_or(Error::MeasureNotFound(cmd.measure_index))?
3588 .tablature_change = cmd.config.clone();
3589 Ok(())
3590}
3591
3592fn apply_set_mid_measure_clefs(
3593 cmd: &SetMidMeasureClefsCmd,
3594 score: &mut Score,
3595) -> Result<(), Error> {
3596 let time_signature = score
3597 .parts
3598 .get(cmd.part_index)
3599 .and_then(|part| part.staves.get(cmd.staff_index))
3600 .map_or_else(
3601 || score.settings.time_signature.clone(),
3602 |staff| {
3603 staff
3604 .meter_at(cmd.measure_index, &score.settings.time_signature)
3605 .clone()
3606 },
3607 );
3608 let measure = score
3609 .parts
3610 .get_mut(cmd.part_index)
3611 .ok_or(Error::PartNotFound(cmd.part_index))?
3612 .staves
3613 .get_mut(cmd.staff_index)
3614 .ok_or(Error::StaffNotFound(cmd.staff_index))?
3615 .measures
3616 .get_mut(cmd.measure_index)
3617 .ok_or(Error::MeasureNotFound(cmd.measure_index))?;
3618 let length = measure.duration_beats(&time_signature);
3619 let mut previous = 0.0;
3620 for change in &cmd.clefs {
3621 match change.offset.beats() {
3622 Some(offset) if offset > previous + 1e-9 && offset < length - 1e-9 => {
3623 previous = offset;
3624 }
3625 _ => {
3626 return Err(Error::InvalidCommand(
3627 "mid-measure clefs must lie strictly inside the measure in increasing order"
3628 .into(),
3629 ));
3630 }
3631 }
3632 }
3633 measure.mid_clefs = cmd.clefs.clone();
3634 Ok(())
3635}
3636
3637fn validate_tablature_config(config: &TablatureConfig) -> Result<(), Error> {
3638 if !(1..=64).contains(&config.lines) {
3639 return Err(Error::InvalidCommand(format!(
3640 "tablature line count {} is outside 1..=64",
3641 config.lines
3642 )));
3643 }
3644 if config.tuning_midi.len() > usize::from(config.lines) {
3645 return Err(Error::InvalidCommand(format!(
3646 "tablature has {} tunings for {} lines",
3647 config.tuning_midi.len(),
3648 config.lines
3649 )));
3650 }
3651 if let Some(midi) = config
3652 .tuning_midi
3653 .iter()
3654 .copied()
3655 .find(|midi| !(0..=127).contains(midi))
3656 {
3657 return Err(Error::InvalidCommand(format!(
3658 "tablature tuning MIDI {midi} is outside 0..=127"
3659 )));
3660 }
3661 Ok(())
3662}
3663
3664fn validate_percussion_kit(instruments: &[PercussionInstrument]) -> Result<(), Error> {
3665 let mut ids = std::collections::HashSet::new();
3666 for instrument in instruments {
3667 if instrument.id.trim().is_empty() {
3668 return Err(Error::InvalidCommand(
3669 "percussion instrument id must not be empty".into(),
3670 ));
3671 }
3672 if !ids.insert(instrument.id.as_str()) {
3673 return Err(Error::InvalidCommand(format!(
3674 "percussion instrument id '{}' is duplicated",
3675 instrument.id
3676 )));
3677 }
3678 if let Some(position) = instrument.staff_position
3679 && !(-32..=32).contains(&position)
3680 {
3681 return Err(Error::InvalidCommand(format!(
3682 "percussion staff position {position} is outside -32..=32"
3683 )));
3684 }
3685 if let Some(voice) = instrument.preferred_voice
3686 && !(1..=4).contains(&voice)
3687 {
3688 return Err(Error::InvalidCommand(format!(
3689 "percussion preferred voice {voice} is outside 1..=4"
3690 )));
3691 }
3692 if instrument
3693 .techniques
3694 .iter()
3695 .any(|technique| technique.trim().is_empty() || technique.len() > 128)
3696 {
3697 return Err(Error::InvalidCommand(
3698 "percussion techniques must be non-empty and at most 128 bytes".into(),
3699 ));
3700 }
3701 }
3702 Ok(())
3703}
3704
3705fn validate_instrument_definition(definition: &InstrumentDefinition) -> Result<(), Error> {
3706 if definition.id.trim().is_empty() {
3707 return Err(Error::InvalidCommand(
3708 "instrument definition id must not be empty".into(),
3709 ));
3710 }
3711 if !(1..=64).contains(&definition.staff_count) {
3712 return Err(Error::InvalidCommand(format!(
3713 "instrument staff count {} is outside 1..=64",
3714 definition.staff_count
3715 )));
3716 }
3717 if definition.midi_channel > 15 {
3718 return Err(Error::InvalidCommand(format!(
3719 "instrument MIDI channel {} is outside 0..=15",
3720 definition.midi_channel
3721 )));
3722 }
3723 for (name, range) in [
3724 ("written", definition.written_range),
3725 ("sounding", definition.sounding_range),
3726 ] {
3727 if let Some(InstrumentRange { lowest, highest }) = range
3728 && lowest > highest
3729 {
3730 return Err(Error::InvalidCommand(format!(
3731 "instrument {name} range has lowest MIDI note {lowest} above highest {highest}"
3732 )));
3733 }
3734 }
3735 Ok(())
3736}
3737
3738fn apply_upsert_score_view(cmd: &UpsertScoreViewCmd, score: &mut Score) -> Result<(), Error> {
3739 validate_score_view(&cmd.view, score)?;
3740 if let Some(index) = score.views.iter().position(|view| view.id == cmd.view.id) {
3741 score.views[index] = cmd.view.clone();
3742 } else {
3743 score.views.push(cmd.view.clone());
3744 }
3745 Ok(())
3746}
3747
3748fn apply_remove_score_view(cmd: &RemoveScoreViewCmd, score: &mut Score) -> Result<(), Error> {
3749 let index = score
3750 .views
3751 .iter()
3752 .position(|view| view.id == cmd.id)
3753 .ok_or_else(|| Error::InvalidCommand(format!("score view '{}' does not exist", cmd.id)))?;
3754 score.views.remove(index);
3755 Ok(())
3756}
3757
3758fn validate_score_view(view: &ScoreView, score: &Score) -> Result<(), Error> {
3759 if view.id.trim().is_empty() || view.name.trim().is_empty() {
3760 return Err(Error::InvalidCommand(
3761 "score view id and name must not be empty".into(),
3762 ));
3763 }
3764 if view.parts.is_empty() {
3765 return Err(Error::InvalidCommand(
3766 "score view must select at least one part".into(),
3767 ));
3768 }
3769 let mut selected = vec![false; score.parts.len()];
3770 for &part in &view.parts {
3771 if part >= score.parts.len() {
3772 return Err(Error::PartNotFound(part));
3773 }
3774 if std::mem::replace(&mut selected[part], true) {
3775 return Err(Error::InvalidCommand(format!(
3776 "score view '{}' selects part {part} more than once",
3777 view.id
3778 )));
3779 }
3780 }
3781 if view.layout.measures_per_row.is_some_and(|value| value == 0) {
3782 return Err(Error::InvalidCommand(
3783 "score view measures per row must be greater than zero".into(),
3784 ));
3785 }
3786 if view
3787 .layout
3788 .typed_style_overrides
3789 .iter()
3790 .any(|override_| !override_.value.is_finite() || !(0.05..=64.0).contains(&override_.value))
3791 {
3792 return Err(Error::InvalidCommand(
3793 "score view typed style override values must be finite and within 0.05..=64".into(),
3794 ));
3795 }
3796 for reference in &view.layout.hidden_staves {
3797 let Some(part) = score.parts.get(reference.part) else {
3798 return Err(Error::PartNotFound(reference.part));
3799 };
3800 if reference.staff >= part.staves.len() {
3801 return Err(Error::StaffNotFound(reference.staff));
3802 }
3803 if !selected[reference.part] {
3804 return Err(Error::InvalidCommand(
3805 "score view cannot hide a staff outside its selected parts".into(),
3806 ));
3807 }
3808 }
3809 let mut overridden = std::collections::HashSet::new();
3810 for override_ in &view.staff_kind_overrides {
3811 let reference = override_.staff;
3812 let Some(part) = score.parts.get(reference.part) else {
3813 return Err(Error::PartNotFound(reference.part));
3814 };
3815 if reference.staff >= part.staves.len() {
3816 return Err(Error::StaffNotFound(reference.staff));
3817 }
3818 if !selected[reference.part] {
3819 return Err(Error::InvalidCommand(
3820 "score view cannot override a staff outside its selected parts".into(),
3821 ));
3822 }
3823 if !overridden.insert((reference.part, reference.staff)) {
3824 return Err(Error::InvalidCommand(
3825 "score view may override each staff kind at most once".into(),
3826 ));
3827 }
3828 if override_.kind == StaffKind::Tablature
3829 && part.staves[reference.staff].tablature.is_none()
3830 {
3831 return Err(Error::InvalidCommand(
3832 "tablature score view requires a tablature configuration".into(),
3833 ));
3834 }
3835 }
3836 let measure_count = score.measure_count();
3837 for &break_index in view
3838 .layout
3839 .system_breaks
3840 .iter()
3841 .chain(view.layout.page_breaks.iter())
3842 {
3843 if break_index >= measure_count {
3844 return Err(Error::InvalidCommand(format!(
3845 "score view break measure {break_index} is out of range"
3846 )));
3847 }
3848 }
3849 for (key, value) in &view.layout.style_overrides {
3850 if key.trim().is_empty() || value.len() > 4096 {
3851 return Err(Error::InvalidCommand(
3852 "score view style overrides need a non-empty key and a value of at most 4096 bytes"
3853 .into(),
3854 ));
3855 }
3856 }
3857 Ok(())
3858}
3859
3860fn apply_set_transpose(cmd: &SetTransposeCmd, score: &mut Score) -> Result<(), Error> {
3861 score
3862 .parts
3863 .get_mut(cmd.part_index)
3864 .ok_or(Error::PartNotFound(cmd.part_index))?
3865 .staves
3866 .get_mut(cmd.staff_index)
3867 .ok_or(Error::StaffNotFound(cmd.staff_index))?
3868 .transpose_semitones = cmd.semitones;
3869 Ok(())
3870}
3871
3872fn apply_transpose_staff_region(
3873 cmd: &TransposeStaffRegionCmd,
3874 score: &mut Score,
3875) -> Result<(), Error> {
3876 *score = transpose_staff_region_checked(
3877 score,
3878 cmd.part_index,
3879 cmd.staff_index,
3880 cmd.start_measure,
3881 cmd.end_measure,
3882 cmd.semitones,
3883 cmd.target,
3884 )?;
3885 Ok(())
3886}
3887
3888fn apply_exchange_voices(cmd: &ExchangeVoicesCmd, score: &mut Score) -> Result<(), Error> {
3889 if cmd.first_voice >= 4 {
3890 return Err(Error::VoiceOutOfRange(cmd.first_voice));
3891 }
3892 if cmd.second_voice >= 4 {
3893 return Err(Error::VoiceOutOfRange(cmd.second_voice));
3894 }
3895 if cmd.first_voice == cmd.second_voice {
3896 return Err(Error::InvalidCommand(
3897 "exchange voices requires two distinct voices".into(),
3898 ));
3899 }
3900 let start = cmd.start_measure.min(cmd.end_measure);
3901 let end = cmd.start_measure.max(cmd.end_measure);
3902 let staff = score
3903 .parts
3904 .get_mut(cmd.part_index)
3905 .ok_or(Error::PartNotFound(cmd.part_index))?
3906 .staves
3907 .get_mut(cmd.staff_index)
3908 .ok_or(Error::StaffNotFound(cmd.staff_index))?;
3909 if end >= staff.measures.len() {
3910 return Err(Error::MeasureNotFound(end));
3911 }
3912 for measure in &mut staff.measures[start..=end] {
3913 measure.voices.swap(cmd.first_voice, cmd.second_voice);
3914 measure
3915 .source_voice_numbers
3916 .swap(cmd.first_voice, cmd.second_voice);
3917 }
3918 remap_spanners(score, |address| {
3919 if address.part != cmd.part_index
3920 || address.staff != cmd.staff_index
3921 || address.measure < start
3922 || address.measure > end
3923 {
3924 return Some(address.clone());
3925 }
3926 let mut remapped = address.clone();
3927 if address.voice == cmd.first_voice {
3928 remapped.voice = cmd.second_voice;
3929 } else if address.voice == cmd.second_voice {
3930 remapped.voice = cmd.first_voice;
3931 }
3932 Some(remapped)
3933 });
3934 Ok(())
3935}
3936
3937fn apply_move_or_copy_voice_range(
3938 cmd: &MoveOrCopyVoiceRangeCmd,
3939 score: &mut Score,
3940) -> Result<(), Error> {
3941 if cmd.source_start.part != cmd.source_end.part
3942 || cmd.source_start.staff != cmd.source_end.staff
3943 || cmd.source_start.voice != cmd.source_end.voice
3944 {
3945 return Err(Error::InvalidCommand(
3946 "move or copy source endpoints must share part, staff, and voice".into(),
3947 ));
3948 }
3949 if cmd.source_start.voice >= 4 {
3950 return Err(Error::VoiceOutOfRange(cmd.source_start.voice));
3951 }
3952 let source_from = cmd.source_start.measure.min(cmd.source_end.measure);
3953 let source_to = cmd.source_start.measure.max(cmd.source_end.measure);
3954 let source_count = source_to - source_from + 1;
3955 let target_end = cmd
3956 .target
3957 .measure
3958 .checked_add(source_count - 1)
3959 .ok_or_else(|| Error::InvalidCommand("voice range target overflows".into()))?;
3960 if cmd.move_source
3961 && cmd.target.part == cmd.source_start.part
3962 && cmd.target.staff == cmd.source_start.staff
3963 && cmd.target.voice == cmd.source_start.voice
3964 && cmd.target.measure <= source_to
3965 && target_end >= source_from
3966 {
3967 return Err(Error::InvalidCommand(
3968 "moving a voice range onto itself is not supported".into(),
3969 ));
3970 }
3971
3972 let fragment = extract_score_fragment(
3973 score,
3974 &[ScoreFragmentSelection {
3975 start: cmd.source_start.clone(),
3976 end: cmd.source_end.clone(),
3977 }],
3978 )?;
3979 apply_paste_score_fragment(
3980 &PasteScoreFragmentCmd {
3981 fragment,
3982 target: cmd.target.clone(),
3983 policy: ScoreFragmentPastePolicy::Replace,
3984 },
3985 score,
3986 )?;
3987 if !cmd.move_source {
3988 return Ok(());
3989 }
3990
3991 let settings_time_signature = score.settings.time_signature.clone();
3992 let staff = score
3993 .parts
3994 .get_mut(cmd.source_start.part)
3995 .ok_or(Error::PartNotFound(cmd.source_start.part))?
3996 .staves
3997 .get_mut(cmd.source_start.staff)
3998 .ok_or(Error::StaffNotFound(cmd.source_start.staff))?;
3999 if source_to >= staff.measures.len() {
4000 return Err(Error::MeasureNotFound(source_to));
4001 }
4002 for measure in &mut staff.measures[source_from..=source_to] {
4003 let signature = measure
4004 .time_sig
4005 .as_ref()
4006 .unwrap_or(&settings_time_signature);
4007 let mut replacement = Vec::new();
4008 pad_voice_to_measure(&mut replacement, signature.total_beats());
4009 measure.voices[cmd.source_start.voice] = replacement;
4010 measure.source_voice_numbers[cmd.source_start.voice] = None;
4011 }
4012 prune_orphaned_spanners(score);
4013 Ok(())
4014}
4015
4016fn effective_staff_measure_beats(
4017 staff: &Staff,
4018 default_time_signature: &TimeSignature,
4019 measure_index: usize,
4020) -> Result<f64, Error> {
4021 let mut time_signature = default_time_signature.clone();
4022 for measure in staff.measures.iter().take(measure_index.saturating_add(1)) {
4023 if let Some(signature) = &measure.time_sig {
4024 time_signature = signature.clone();
4025 }
4026 }
4027 if staff.measures.get(measure_index).is_none() {
4028 return Err(Error::MeasureNotFound(measure_index));
4029 }
4030 Ok(time_signature.total_beats())
4031}
4032
4033fn normalized_structural_measure_range(start: usize, end: usize) -> (usize, usize) {
4034 (start.min(end), start.max(end))
4035}
4036
4037fn apply_implode_staves(cmd: &ImplodeStavesCmd, score: &mut Score) -> Result<(), Error> {
4038 if !(2..=4).contains(&cmd.source_staves.len()) {
4039 return Err(Error::InvalidCommand(
4040 "implode requires two to four source staves".into(),
4041 ));
4042 }
4043 let mut unique_staves = BTreeSet::new();
4044 for &staff_index in &cmd.source_staves {
4045 if !unique_staves.insert(staff_index) {
4046 return Err(Error::InvalidCommand(
4047 "implode source staves must be distinct".into(),
4048 ));
4049 }
4050 }
4051 if !unique_staves.contains(&cmd.target_staff) {
4052 return Err(Error::InvalidCommand(
4053 "implode target staff must be included in source staves".into(),
4054 ));
4055 }
4056 let (start, end) = normalized_structural_measure_range(cmd.start_measure, cmd.end_measure);
4057 let part = score
4058 .parts
4059 .get(cmd.part_index)
4060 .ok_or(Error::PartNotFound(cmd.part_index))?;
4061 for &staff_index in &cmd.source_staves {
4062 if part.staves.get(staff_index).is_none() {
4063 return Err(Error::StaffNotFound(staff_index));
4064 }
4065 }
4066
4067 for measure_index in start..=end {
4071 let mut expected_beats: Option<f64> = None;
4072 for &staff_index in &cmd.source_staves {
4073 let staff = &part.staves[staff_index];
4074 let measure = staff
4075 .measures
4076 .get(measure_index)
4077 .ok_or(Error::MeasureNotFound(measure_index))?;
4078 let beats = effective_staff_measure_beats(
4079 staff,
4080 &score.settings.time_signature,
4081 measure_index,
4082 )?;
4083 if let Some(expected) = expected_beats
4084 && (expected - beats).abs() > 1e-9
4085 {
4086 return Err(Error::InvalidCommand(
4087 "implode source staves must have matching measure durations".into(),
4088 ));
4089 }
4090 expected_beats = Some(beats);
4091 if measure.voices[1..].iter().any(|voice| !voice.is_empty()) {
4092 return Err(Error::InvalidCommand(
4093 "implode requires empty secondary source voices".into(),
4094 ));
4095 }
4096 if measure.voices[0]
4097 .iter()
4098 .any(|note| note.cross_staff.is_some())
4099 {
4100 return Err(Error::InvalidCommand(
4101 "implode does not support cross-staff source notes".into(),
4102 ));
4103 }
4104 let total: f64 = measure.voices[0].iter().map(Note::beats).sum();
4105 if total > beats + 1e-9 {
4106 return Err(Error::InvalidCommand(
4107 "implode source voice exceeds its measure duration".into(),
4108 ));
4109 }
4110 }
4111 }
4112
4113 for measure_index in start..=end {
4114 let transferred: Vec<(Vec<Note>, Option<u32>)> = cmd
4115 .source_staves
4116 .iter()
4117 .map(|&staff_index| {
4118 let measure =
4119 &score.parts[cmd.part_index].staves[staff_index].measures[measure_index];
4120 (measure.voices[0].clone(), measure.source_voice_numbers[0])
4121 })
4122 .collect();
4123 {
4124 let target =
4125 &mut score.parts[cmd.part_index].staves[cmd.target_staff].measures[measure_index];
4126 target.voices = [Vec::new(), Vec::new(), Vec::new(), Vec::new()];
4127 target.source_voice_numbers = [None; 4];
4128 for (voice_index, (notes, source_voice_number)) in transferred.into_iter().enumerate() {
4129 target.voices[voice_index] = notes;
4130 target.source_voice_numbers[voice_index] = source_voice_number;
4131 }
4132 }
4133 for &staff_index in &cmd.source_staves {
4134 if staff_index == cmd.target_staff {
4135 continue;
4136 }
4137 let beats = effective_staff_measure_beats(
4138 &score.parts[cmd.part_index].staves[staff_index],
4139 &score.settings.time_signature,
4140 measure_index,
4141 )?;
4142 let mut rest_voice = Vec::new();
4143 pad_voice_to_measure(&mut rest_voice, beats);
4144 let measure =
4145 &mut score.parts[cmd.part_index].staves[staff_index].measures[measure_index];
4146 measure.voices = [rest_voice, Vec::new(), Vec::new(), Vec::new()];
4147 measure.source_voice_numbers = [None; 4];
4148 }
4149 }
4150 remap_spanners(score, |address| {
4151 if address.part != cmd.part_index || address.measure < start || address.measure > end {
4152 return Some(address.clone());
4153 }
4154 let voice = cmd
4155 .source_staves
4156 .iter()
4157 .position(|&staff| staff == address.staff);
4158 if address.voice != 0 {
4159 return Some(address.clone());
4160 }
4161 voice
4162 .map(|voice| NoteAddr {
4163 staff: cmd.target_staff,
4164 voice,
4165 ..address.clone()
4166 })
4167 .or_else(|| Some(address.clone()))
4168 });
4169 Ok(())
4170}
4171
4172fn apply_explode_voices(cmd: &ExplodeVoicesCmd, score: &mut Score) -> Result<(), Error> {
4173 if !(2..=4).contains(&cmd.target_staves.len()) {
4174 return Err(Error::InvalidCommand(
4175 "explode requires two to four target staves".into(),
4176 ));
4177 }
4178 if cmd.target_staves.first() != Some(&cmd.source_staff) {
4179 return Err(Error::InvalidCommand(
4180 "explode target staves must begin with the source staff".into(),
4181 ));
4182 }
4183 let mut unique_staves = BTreeSet::new();
4184 for &staff_index in &cmd.target_staves {
4185 if !unique_staves.insert(staff_index) {
4186 return Err(Error::InvalidCommand(
4187 "explode target staves must be distinct".into(),
4188 ));
4189 }
4190 }
4191 let (start, end) = normalized_structural_measure_range(cmd.start_measure, cmd.end_measure);
4192 let part = score
4193 .parts
4194 .get(cmd.part_index)
4195 .ok_or(Error::PartNotFound(cmd.part_index))?;
4196 let source = part
4197 .staves
4198 .get(cmd.source_staff)
4199 .ok_or(Error::StaffNotFound(cmd.source_staff))?;
4200 for &staff_index in &cmd.target_staves {
4201 if part.staves.get(staff_index).is_none() {
4202 return Err(Error::StaffNotFound(staff_index));
4203 }
4204 }
4205
4206 for measure_index in start..=end {
4207 let expected =
4208 effective_staff_measure_beats(source, &score.settings.time_signature, measure_index)?;
4209 let source_measure = source
4210 .measures
4211 .get(measure_index)
4212 .ok_or(Error::MeasureNotFound(measure_index))?;
4213 for voice_index in cmd.target_staves.len()..4 {
4214 if !source_measure.voices[voice_index].is_empty() {
4215 return Err(Error::InvalidCommand(
4216 "explode would discard a source voice without a target staff".into(),
4217 ));
4218 }
4219 }
4220 for voice_index in 0..cmd.target_staves.len() {
4221 if source_measure.voices[voice_index]
4222 .iter()
4223 .any(|note| note.cross_staff.is_some())
4224 {
4225 return Err(Error::InvalidCommand(
4226 "explode does not support cross-staff source notes".into(),
4227 ));
4228 }
4229 let total: f64 = source_measure.voices[voice_index]
4230 .iter()
4231 .map(Note::beats)
4232 .sum();
4233 if total > expected + 1e-9 {
4234 return Err(Error::InvalidCommand(
4235 "explode source voice exceeds its measure duration".into(),
4236 ));
4237 }
4238 }
4239 for &staff_index in cmd.target_staves.iter().skip(1) {
4240 let target = &part.staves[staff_index];
4241 let target_beats = effective_staff_measure_beats(
4242 target,
4243 &score.settings.time_signature,
4244 measure_index,
4245 )?;
4246 if (target_beats - expected).abs() > 1e-9 {
4247 return Err(Error::InvalidCommand(
4248 "explode target staves must have matching measure durations".into(),
4249 ));
4250 }
4251 let measure = target
4252 .measures
4253 .get(measure_index)
4254 .ok_or(Error::MeasureNotFound(measure_index))?;
4255 if measure.voices[0].iter().any(|note| !note.is_rest)
4256 || measure.voices[1..].iter().any(|voice| !voice.is_empty())
4257 || measure.source_voice_numbers.iter().any(Option::is_some)
4258 {
4259 return Err(Error::InvalidCommand(
4260 "explode destination staff must contain only an unnumbered rest voice".into(),
4261 ));
4262 }
4263 }
4264 }
4265
4266 for measure_index in start..=end {
4267 let transferred: Vec<(Vec<Note>, Option<u32>)> = (0..cmd.target_staves.len())
4268 .map(|voice_index| {
4269 let measure =
4270 &score.parts[cmd.part_index].staves[cmd.source_staff].measures[measure_index];
4271 (
4272 measure.voices[voice_index].clone(),
4273 measure.source_voice_numbers[voice_index],
4274 )
4275 })
4276 .collect();
4277 for (voice_index, &staff_index) in cmd.target_staves.iter().enumerate() {
4278 let (notes, source_voice_number) = &transferred[voice_index];
4279 let target =
4280 &mut score.parts[cmd.part_index].staves[staff_index].measures[measure_index];
4281 target.voices = [notes.clone(), Vec::new(), Vec::new(), Vec::new()];
4282 target.source_voice_numbers = [*source_voice_number, None, None, None];
4283 }
4284 }
4285 remap_spanners(score, |address| {
4286 if address.part != cmd.part_index
4287 || address.staff != cmd.source_staff
4288 || address.measure < start
4289 || address.measure > end
4290 || address.voice >= cmd.target_staves.len()
4291 {
4292 return Some(address.clone());
4293 }
4294 Some(NoteAddr {
4295 staff: cmd.target_staves[address.voice],
4296 voice: 0,
4297 ..address.clone()
4298 })
4299 });
4300 Ok(())
4301}
4302
4303fn clear_derived_chord_note_notation(note: &mut Note) {
4304 note.articulations.clear();
4305 note.dynamic = None;
4306 note.stem_up = None;
4307 note.hairpin_start = None;
4308 note.hairpin_end = false;
4309 note.chord_symbol = None;
4310 note.ottava_start = None;
4311 note.ottava_end = false;
4312 note.lyric = None;
4313 note.additional_lyrics.clear();
4314 note.pedal_start = false;
4315 note.pedal_end = false;
4316 note.slur_start = false;
4317 note.slur_end = false;
4318 note.arpeggiate = None;
4319 note.technique_text = None;
4320 note.glissando_start = false;
4321 note.glissando_end = false;
4322 note.cross_staff = None;
4323 note.fingering = None;
4324 note.fingerings.clear();
4325 note.string_number = None;
4326 note.trill_line_start = false;
4327 note.trill_line_end = false;
4328 note.guitar_technique = None;
4329 note.guitar_bend_alter_cents = None;
4330 note.guitar_bend_curve.clear();
4331}
4332
4333fn exploded_chord_note(source: &Note, pitch_index: usize) -> Note {
4334 if pitch_index == 0 {
4335 let mut retained = source.clone();
4336 if !retained.is_rest {
4337 retained.pitches = vec![source.pitches[0].clone()];
4338 retained.tab_positions = source.tab_positions.first().cloned().into_iter().collect();
4339 retained.tab_position = retained.tab_positions.first().cloned();
4340 }
4341 return retained;
4342 }
4343 let mut derived = source.clone();
4344 derived.id = Uuid::new_v4().to_string();
4345 clear_derived_chord_note_notation(&mut derived);
4346 if derived.is_rest || pitch_index >= source.pitches.len() {
4347 derived.is_rest = true;
4348 derived.is_unpitched = false;
4349 derived.pitches.clear();
4350 derived.tab_position = None;
4351 derived.tab_positions.clear();
4352 } else {
4353 derived.pitches = vec![source.pitches[pitch_index].clone()];
4354 derived.tab_positions = source
4355 .tab_positions
4356 .get(pitch_index)
4357 .cloned()
4358 .into_iter()
4359 .collect();
4360 derived.tab_position = derived.tab_positions.first().cloned();
4361 }
4362 derived
4363}
4364
4365fn apply_explode_chord_pitches(
4366 cmd: &ExplodeChordPitchesCmd,
4367 score: &mut Score,
4368) -> Result<(), Error> {
4369 if !(2..=4).contains(&cmd.target_staves.len()) {
4370 return Err(Error::InvalidCommand(
4371 "chord explode requires two to four target staves".into(),
4372 ));
4373 }
4374 if cmd.target_staves.first() != Some(&cmd.source_staff) {
4375 return Err(Error::InvalidCommand(
4376 "chord explode target staves must begin with the source staff".into(),
4377 ));
4378 }
4379 let mut unique_staves = BTreeSet::new();
4380 for &staff_index in &cmd.target_staves {
4381 if !unique_staves.insert(staff_index) {
4382 return Err(Error::InvalidCommand(
4383 "chord explode target staves must be distinct".into(),
4384 ));
4385 }
4386 }
4387 let (start, end) = normalized_structural_measure_range(cmd.start_measure, cmd.end_measure);
4388 let part = score
4389 .parts
4390 .get(cmd.part_index)
4391 .ok_or(Error::PartNotFound(cmd.part_index))?;
4392 let source = part
4393 .staves
4394 .get(cmd.source_staff)
4395 .ok_or(Error::StaffNotFound(cmd.source_staff))?;
4396 for &staff_index in &cmd.target_staves {
4397 if part.staves.get(staff_index).is_none() {
4398 return Err(Error::StaffNotFound(staff_index));
4399 }
4400 }
4401 for measure_index in start..=end {
4402 let expected =
4403 effective_staff_measure_beats(source, &score.settings.time_signature, measure_index)?;
4404 let source_measure = source
4405 .measures
4406 .get(measure_index)
4407 .ok_or(Error::MeasureNotFound(measure_index))?;
4408 for note in &source_measure.voices[0] {
4409 if note.tuplet.is_some() {
4410 return Err(Error::InvalidCommand(
4411 "chord explode does not support tuplets".into(),
4412 ));
4413 }
4414 if note.cross_staff.is_some() {
4415 return Err(Error::InvalidCommand(
4416 "chord explode does not support cross-staff source notes".into(),
4417 ));
4418 }
4419 if note.is_unpitched || note.is_grace || note.is_cue {
4420 return Err(Error::InvalidCommand(
4421 "chord explode does not support unpitched, grace, or cue notes".into(),
4422 ));
4423 }
4424 if !note.is_rest
4425 && (note.pitches.is_empty() || note.pitches.len() > cmd.target_staves.len())
4426 {
4427 return Err(Error::InvalidCommand(
4428 "chord explode pitch count must fit target staves".into(),
4429 ));
4430 }
4431 }
4432 let total: f64 = source_measure.voices[0].iter().map(Note::beats).sum();
4433 if total > expected + 1e-9 {
4434 return Err(Error::InvalidCommand(
4435 "chord explode source voice exceeds its measure duration".into(),
4436 ));
4437 }
4438 for &staff_index in cmd.target_staves.iter().skip(1) {
4439 let target = &part.staves[staff_index];
4440 let target_beats = effective_staff_measure_beats(
4441 target,
4442 &score.settings.time_signature,
4443 measure_index,
4444 )?;
4445 if (target_beats - expected).abs() > 1e-9 {
4446 return Err(Error::InvalidCommand(
4447 "chord explode target staves must have matching measure durations".into(),
4448 ));
4449 }
4450 let measure = target
4451 .measures
4452 .get(measure_index)
4453 .ok_or(Error::MeasureNotFound(measure_index))?;
4454 if measure.voices[0].iter().any(|note| !note.is_rest)
4455 || measure.voices[1..].iter().any(|voice| !voice.is_empty())
4456 || measure.source_voice_numbers.iter().any(Option::is_some)
4457 {
4458 return Err(Error::InvalidCommand(
4459 "chord explode destination staff must contain only an unnumbered rest voice"
4460 .into(),
4461 ));
4462 }
4463 }
4464 }
4465 for measure_index in start..=end {
4466 let source_measure =
4467 &score.parts[cmd.part_index].staves[cmd.source_staff].measures[measure_index];
4468 let source_voice_number = source_measure.source_voice_numbers[0];
4469 let mut exploded = vec![Vec::new(); cmd.target_staves.len()];
4470 for note in &source_measure.voices[0] {
4471 for (pitch_index, voice) in exploded.iter_mut().enumerate() {
4472 voice.push(exploded_chord_note(note, pitch_index));
4473 }
4474 }
4475 for (index, &staff_index) in cmd.target_staves.iter().enumerate() {
4476 let target =
4477 &mut score.parts[cmd.part_index].staves[staff_index].measures[measure_index];
4478 target.voices = [exploded[index].clone(), Vec::new(), Vec::new(), Vec::new()];
4479 target.source_voice_numbers = [source_voice_number, None, None, None];
4480 }
4481 }
4482 Ok(())
4483}
4484
4485fn scaled_duration(duration: &Duration, scale: DurationScale) -> Option<Duration> {
4486 const ORDER: [Duration; 9] = [
4487 Duration::Breve,
4488 Duration::Whole,
4489 Duration::Half,
4490 Duration::Quarter,
4491 Duration::Eighth,
4492 Duration::Sixteenth,
4493 Duration::ThirtySecond,
4494 Duration::SixtyFourth,
4495 Duration::HundredTwentyEighth,
4496 ];
4497 let index = ORDER.iter().position(|value| value == duration)?;
4498 let scaled = match scale {
4499 DurationScale::Double => index.checked_sub(1)?,
4500 DurationScale::Half => index + 1,
4501 };
4502 ORDER.get(scaled).cloned()
4503}
4504
4505fn uniform_tuplet_ratio(voice: &[Note]) -> Result<Option<TupletInfo>, Error> {
4506 let mut ratio: Option<TupletInfo> = None;
4507 for note in voice {
4508 let Some(tuplet) = ¬e.tuplet else {
4509 continue;
4510 };
4511 if tuplet.actual_notes == 0 || tuplet.normal_notes == 0 {
4512 return Err(Error::InvalidCommand(
4513 "duration scaling requires a non-zero tuplet ratio".into(),
4514 ));
4515 }
4516 if let Some(existing) = &ratio {
4517 if existing != tuplet {
4518 return Err(Error::InvalidCommand(
4519 "duration scaling requires one shared tuplet ratio per voice".into(),
4520 ));
4521 }
4522 } else {
4523 ratio = Some(tuplet.clone());
4524 }
4525 }
4526 Ok(ratio)
4527}
4528
4529fn pad_voice_to_measure_with_tuplet_ratio(
4530 voice: &mut Vec<Note>,
4531 max_beats: f64,
4532 ratio: &TupletInfo,
4533) -> Result<(), Error> {
4534 let mut used: f64 = voice.iter().map(Note::beats).sum();
4535 let ratio_scale = f64::from(ratio.normal_notes) / f64::from(ratio.actual_notes);
4536 while max_beats - used > 1e-9 {
4537 let remaining = max_beats - used;
4538 let duration = [
4539 Duration::Whole,
4540 Duration::Half,
4541 Duration::Quarter,
4542 Duration::Eighth,
4543 Duration::Sixteenth,
4544 Duration::ThirtySecond,
4545 Duration::SixtyFourth,
4546 Duration::HundredTwentyEighth,
4547 ]
4548 .into_iter()
4549 .find(|duration| duration.beats(0) * ratio_scale <= remaining + 1e-9)
4550 .ok_or_else(|| {
4551 Error::InvalidCommand(
4552 "duration scaling cannot represent the remaining tuplet duration".into(),
4553 )
4554 })?;
4555 let mut rest = Note::rest(duration);
4556 rest.tuplet = Some(ratio.clone());
4557 used += rest.beats();
4558 voice.push(rest);
4559 }
4560 Ok(())
4561}
4562
4563fn apply_scale_voice_range(cmd: &ScaleVoiceRangeCmd, score: &mut Score) -> Result<(), Error> {
4564 if cmd.voice >= 4 {
4565 return Err(Error::VoiceOutOfRange(cmd.voice));
4566 }
4567 let (start, end) = normalized_structural_measure_range(cmd.start_measure, cmd.end_measure);
4568 let staff = score
4569 .parts
4570 .get(cmd.part_index)
4571 .ok_or(Error::PartNotFound(cmd.part_index))?
4572 .staves
4573 .get(cmd.staff_index)
4574 .ok_or(Error::StaffNotFound(cmd.staff_index))?;
4575 for measure_index in start..=end {
4576 let measure = staff
4577 .measures
4578 .get(measure_index)
4579 .ok_or(Error::MeasureNotFound(measure_index))?;
4580 let expected =
4581 effective_staff_measure_beats(staff, &score.settings.time_signature, measure_index)?;
4582 let voice = &measure.voices[cmd.voice];
4583 match cmd.tuplet_policy {
4584 TupletScalePolicy::PreserveRatio => {
4585 uniform_tuplet_ratio(voice)?;
4586 }
4587 }
4588 let mut scaled_beats = 0.0;
4589 for note in voice {
4590 if scaled_duration(¬e.duration, cmd.scale).is_none() {
4591 return Err(Error::InvalidCommand(
4592 "duration scaling exceeds the portable duration range".into(),
4593 ));
4594 }
4595 if !note.is_grace && !note.is_cue {
4596 scaled_beats += match cmd.scale {
4597 DurationScale::Half => note.beats() / 2.0,
4598 DurationScale::Double => note.beats() * 2.0,
4599 };
4600 }
4601 }
4602 if scaled_beats > expected + 1e-9 {
4603 return Err(Error::InvalidCommand(format!(
4604 "duration scaling overflows measure {measure_index}: {scaled_beats} beats exceeds {expected}"
4605 )));
4606 }
4607 }
4608
4609 for measure_index in start..=end {
4610 let expected = effective_staff_measure_beats(
4611 &score.parts[cmd.part_index].staves[cmd.staff_index],
4612 &score.settings.time_signature,
4613 measure_index,
4614 )?;
4615 let voice = &mut score.parts[cmd.part_index].staves[cmd.staff_index].measures
4616 [measure_index]
4617 .voices[cmd.voice];
4618 if voice.is_empty() {
4619 continue;
4620 }
4621 let tuplet_ratio = match cmd.tuplet_policy {
4622 TupletScalePolicy::PreserveRatio => uniform_tuplet_ratio(voice)?,
4623 };
4624 for note in voice.iter_mut() {
4625 note.duration = scaled_duration(¬e.duration, cmd.scale).ok_or_else(|| {
4626 Error::InvalidCommand("duration scaling exceeds the portable duration range".into())
4627 })?;
4628 }
4629 if let Some(ratio) = tuplet_ratio {
4630 pad_voice_to_measure_with_tuplet_ratio(voice, expected, &ratio)?;
4631 } else {
4632 pad_voice_to_measure(voice, expected);
4633 }
4634 }
4635 Ok(())
4636}
4637
4638fn apply_paste_score_fragment(cmd: &PasteScoreFragmentCmd, score: &mut Score) -> Result<(), Error> {
4639 if !(MIN_SUPPORTED_SCORE_FRAGMENT_CONTRACT_VERSION..=SCORE_FRAGMENT_CONTRACT_VERSION)
4640 .contains(&cmd.fragment.contract_version)
4641 {
4642 return Err(Error::InvalidCommand(format!(
4643 "unsupported score fragment contract version {}",
4644 cmd.fragment.contract_version
4645 )));
4646 }
4647 if cmd.fragment.voices.is_empty() {
4648 return Err(Error::InvalidCommand(
4649 "cannot paste an empty score fragment".into(),
4650 ));
4651 }
4652
4653 let mut lanes = BTreeSet::new();
4657 let mut replaced = BTreeSet::new();
4658 for lane in &cmd.fragment.voices {
4659 let part_index = cmd
4660 .target
4661 .part
4662 .checked_add(lane.relative_part)
4663 .ok_or_else(|| Error::InvalidCommand("fragment part target overflows".into()))?;
4664 let staff_index = cmd
4665 .target
4666 .staff
4667 .checked_add(lane.relative_staff)
4668 .ok_or_else(|| Error::InvalidCommand("fragment staff target overflows".into()))?;
4669 let voice_index = cmd
4670 .target
4671 .voice
4672 .checked_add(lane.relative_voice)
4673 .ok_or_else(|| Error::InvalidCommand("fragment voice target overflows".into()))?;
4674 if voice_index >= 4 {
4675 return Err(Error::VoiceOutOfRange(voice_index));
4676 }
4677 let staff = score
4678 .parts
4679 .get(part_index)
4680 .ok_or(Error::PartNotFound(part_index))?
4681 .staves
4682 .get(staff_index)
4683 .ok_or(Error::StaffNotFound(staff_index))?;
4684 if !lanes.insert((part_index, staff_index, voice_index)) {
4685 return Err(Error::InvalidCommand(
4686 "fragment contains duplicate destination voice lanes".into(),
4687 ));
4688 }
4689 for measure in &lane.measures {
4690 let measure_index = cmd
4691 .target
4692 .measure
4693 .checked_add(measure.relative_measure)
4694 .ok_or_else(|| Error::InvalidCommand("fragment measure target overflows".into()))?;
4695 if measure_index >= staff.measures.len() {
4696 return Err(Error::MeasureNotFound(measure_index));
4697 }
4698 if !measure.cross_staff_targets.is_empty()
4699 && measure.cross_staff_targets.len() != measure.notes.len()
4700 {
4701 return Err(Error::InvalidCommand(
4702 "fragment cross-staff targets do not match note count".into(),
4703 ));
4704 }
4705 if cmd.policy == ScoreFragmentPastePolicy::Merge
4706 && !measure.notes.is_empty()
4707 && staff.measures[measure_index].voices[voice_index]
4708 .iter()
4709 .any(|note| !note.is_rest)
4710 {
4711 return Err(Error::InvalidCommand(
4712 "fragment merge would overwrite a sounding destination lane".into(),
4713 ));
4714 }
4715 if cmd.policy == ScoreFragmentPastePolicy::Merge
4716 && measure.attributes.present
4717 && measure.attributes
4718 != super::fragment::ScoreFragmentMeasureAttributes::from_measure(
4719 &staff.measures[measure_index],
4720 )
4721 {
4722 return Err(Error::InvalidCommand(
4723 "fragment merge would overwrite destination measure attributes".into(),
4724 ));
4725 }
4726 for cross_staff in measure.cross_staff_targets.iter().flatten() {
4727 let target_staff = staff_index as i64 + cross_staff.staff_offset;
4728 if target_staff < 0
4729 || score.parts[part_index]
4730 .staves
4731 .get(target_staff as usize)
4732 .is_none()
4733 {
4734 return Err(Error::InvalidCommand(
4735 "fragment cross-staff target is outside destination part".into(),
4736 ));
4737 }
4738 if cross_staff.target_voice.is_some_and(|voice| voice >= 4) {
4739 return Err(Error::InvalidCommand(
4740 "fragment cross-staff target voice is outside editable range".into(),
4741 ));
4742 }
4743 }
4744 replaced.insert((part_index, staff_index, measure_index, voice_index));
4745 }
4746 }
4747
4748 score.spanners.retain(|spanner| {
4752 !replaced.contains(&(
4753 spanner.start.part,
4754 spanner.start.staff,
4755 spanner.start.measure,
4756 spanner.start.voice,
4757 )) && !replaced.contains(&(
4758 spanner.end.part,
4759 spanner.end.staff,
4760 spanner.end.measure,
4761 spanner.end.voice,
4762 ))
4763 });
4764 for lane in &cmd.fragment.voices {
4765 let part_index = cmd.target.part + lane.relative_part;
4766 let staff_index = cmd.target.staff + lane.relative_staff;
4767 let voice_index = cmd.target.voice + lane.relative_voice;
4768 let staff = &mut score.parts[part_index].staves[staff_index];
4769 for measure in &lane.measures {
4770 let measure_index = cmd.target.measure + measure.relative_measure;
4771 let target = &mut staff.measures[measure_index];
4772 if cmd.policy == ScoreFragmentPastePolicy::Merge && measure.notes.is_empty() {
4773 continue;
4774 }
4775 let mut notes = measure.notes.clone();
4776 for note in &mut notes {
4777 note.id = Uuid::new_v4().to_string();
4778 }
4779 if !measure.cross_staff_targets.is_empty() {
4780 for (note, cross_staff) in notes.iter_mut().zip(&measure.cross_staff_targets) {
4781 note.cross_staff = cross_staff.as_ref().map(|cross_staff| CrossStaff {
4782 target_staff: (staff_index as i64 + cross_staff.staff_offset) as usize,
4783 target_voice: cross_staff.target_voice,
4784 });
4785 }
4786 }
4787 target.voices[voice_index] = notes;
4788 target.source_voice_numbers[voice_index] = measure.source_voice_number;
4789 if cmd.policy == ScoreFragmentPastePolicy::Replace {
4790 measure.attributes.apply_to_measure(target);
4791 }
4792 }
4793 }
4794
4795 let mut used_spanner_ids = score
4796 .spanners
4797 .iter()
4798 .map(|spanner| spanner.id.clone())
4799 .collect::<BTreeSet<_>>();
4800 for source in &cmd.fragment.spanners {
4801 let mut copied = source.clone();
4802 copied.start = fragment_destination_address(&cmd.target, &source.start)?;
4803 copied.end = fragment_destination_address(&cmd.target, &source.end)?;
4804 if note_at(score, &copied.start).is_none() {
4805 return Err(Error::NoteNotFound(copied.start.note));
4806 }
4807 if note_at(score, &copied.end).is_none() {
4808 return Err(Error::NoteNotFound(copied.end.note));
4809 }
4810 copied.id = unique_fragment_spanner_id(&used_spanner_ids, &source.id);
4811 used_spanner_ids.insert(copied.id.clone());
4812 score.spanners.push(copied);
4813 }
4814 Ok(())
4815}
4816
4817fn fragment_destination_address(target: &NoteAddr, relative: &NoteAddr) -> Result<NoteAddr, Error> {
4818 Ok(NoteAddr {
4819 part: target
4820 .part
4821 .checked_add(relative.part)
4822 .ok_or_else(|| Error::InvalidCommand("fragment part target overflows".into()))?,
4823 staff: target
4824 .staff
4825 .checked_add(relative.staff)
4826 .ok_or_else(|| Error::InvalidCommand("fragment staff target overflows".into()))?,
4827 measure: target
4828 .measure
4829 .checked_add(relative.measure)
4830 .ok_or_else(|| Error::InvalidCommand("fragment measure target overflows".into()))?,
4831 voice: target
4832 .voice
4833 .checked_add(relative.voice)
4834 .ok_or_else(|| Error::InvalidCommand("fragment voice target overflows".into()))?,
4835 note: relative.note,
4836 })
4837}
4838
4839fn unique_fragment_spanner_id(used: &BTreeSet<String>, source_id: &str) -> String {
4840 let base = format!("{source_id}-copy");
4841 if !used.contains(&base) {
4842 return base;
4843 }
4844 let mut suffix = 2usize;
4845 loop {
4846 let candidate = format!("{base}-{suffix}");
4847 if !used.contains(&candidate) {
4848 return candidate;
4849 }
4850 suffix += 1;
4851 }
4852}
4853
4854fn apply_toggle_slur(cmd: &ToggleSlurCmd, score: &mut Score) -> Result<(), Error> {
4855 let new_start = !{
4856 score
4857 .parts
4858 .get(cmd.start.part)
4859 .ok_or(Error::PartNotFound(cmd.start.part))?
4860 .staves
4861 .get(cmd.start.staff)
4862 .ok_or(Error::StaffNotFound(cmd.start.staff))?
4863 .measures
4864 .get(cmd.start.measure)
4865 .ok_or(Error::MeasureNotFound(cmd.start.measure))?
4866 .voices
4867 .get(cmd.start.voice)
4868 .ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
4869 .get(cmd.start.note)
4870 .ok_or(Error::NoteNotFound(cmd.start.note))?
4871 .slur_start
4872 };
4873 let new_end = !{
4874 score
4875 .parts
4876 .get(cmd.end.part)
4877 .ok_or(Error::PartNotFound(cmd.end.part))?
4878 .staves
4879 .get(cmd.end.staff)
4880 .ok_or(Error::StaffNotFound(cmd.end.staff))?
4881 .measures
4882 .get(cmd.end.measure)
4883 .ok_or(Error::MeasureNotFound(cmd.end.measure))?
4884 .voices
4885 .get(cmd.end.voice)
4886 .ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
4887 .get(cmd.end.note)
4888 .ok_or(Error::NoteNotFound(cmd.end.note))?
4889 .slur_end
4890 };
4891 score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
4892 [cmd.start.voice][cmd.start.note]
4893 .slur_start = new_start;
4894 score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
4895 [cmd.end.voice][cmd.end.note]
4896 .slur_end = new_end;
4897 Ok(())
4898}
4899
4900fn apply_toggle_trill_line(cmd: &ToggleTrillLineCmd, score: &mut Score) -> Result<(), Error> {
4901 let new_start = !{
4902 score
4903 .parts
4904 .get(cmd.start.part)
4905 .ok_or(Error::PartNotFound(cmd.start.part))?
4906 .staves
4907 .get(cmd.start.staff)
4908 .ok_or(Error::StaffNotFound(cmd.start.staff))?
4909 .measures
4910 .get(cmd.start.measure)
4911 .ok_or(Error::MeasureNotFound(cmd.start.measure))?
4912 .voices
4913 .get(cmd.start.voice)
4914 .ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
4915 .get(cmd.start.note)
4916 .ok_or(Error::NoteNotFound(cmd.start.note))?
4917 .trill_line_start
4918 };
4919 let new_end = !{
4920 score
4921 .parts
4922 .get(cmd.end.part)
4923 .ok_or(Error::PartNotFound(cmd.end.part))?
4924 .staves
4925 .get(cmd.end.staff)
4926 .ok_or(Error::StaffNotFound(cmd.end.staff))?
4927 .measures
4928 .get(cmd.end.measure)
4929 .ok_or(Error::MeasureNotFound(cmd.end.measure))?
4930 .voices
4931 .get(cmd.end.voice)
4932 .ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
4933 .get(cmd.end.note)
4934 .ok_or(Error::NoteNotFound(cmd.end.note))?
4935 .trill_line_end
4936 };
4937 score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
4938 [cmd.start.voice][cmd.start.note]
4939 .trill_line_start = new_start;
4940 score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
4941 [cmd.end.voice][cmd.end.note]
4942 .trill_line_end = new_end;
4943 Ok(())
4944}
4945
4946fn apply_add_staff(cmd: &AddStaffCmd, score: &mut Score) -> Result<(), Error> {
4947 let ts = score.settings.time_signature.clone();
4948 let measure_count = score
4949 .parts
4950 .get(cmd.part_index)
4951 .ok_or(Error::PartNotFound(cmd.part_index))?
4952 .staves
4953 .first()
4954 .map_or(0, |s| s.measures.len());
4955 let mut staff = Staff::new(cmd.clef.clone());
4956 for i in 0..measure_count {
4957 let mut m = Measure::empty(ts.numerator, ts.denominator);
4958 m.number = i as u32 + 1;
4959 staff.measures.push(m);
4960 }
4961 score.parts[cmd.part_index].staves.push(staff);
4962 Ok(())
4963}
4964
4965fn apply_delete_staff(cmd: &DeleteStaffCmd, score: &mut Score) -> Result<(), Error> {
4966 let part = score
4967 .parts
4968 .get_mut(cmd.part_index)
4969 .ok_or(Error::PartNotFound(cmd.part_index))?;
4970 if part.staves.len() <= 1 {
4971 return Err(Error::CannotDeleteLastStaff);
4972 }
4973 if cmd.staff_index >= part.staves.len() {
4974 return Err(Error::StaffNotFound(cmd.staff_index));
4975 }
4976 part.staves.remove(cmd.staff_index);
4977 remap_spanners(score, |address| {
4978 if address.part != cmd.part_index {
4979 Some(address.clone())
4980 } else if address.staff == cmd.staff_index {
4981 None
4982 } else if address.staff > cmd.staff_index {
4983 let mut shifted = address.clone();
4984 shifted.staff -= 1;
4985 Some(shifted)
4986 } else {
4987 Some(address.clone())
4988 }
4989 });
4990 Ok(())
4991}
4992
4993fn same_voice(address: &NoteAddr, part: usize, staff: usize, measure: usize, voice: usize) -> bool {
4995 address.part == part
4996 && address.staff == staff
4997 && address.measure == measure
4998 && address.voice == voice
4999}
5000
5001fn trim_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
5002 if matches!(voice.as_slice(), [only] if only.is_plain_whole_rest()) {
5004 return;
5005 }
5006 let mut total = 0.0f64;
5007 let mut cutoff = voice.len();
5008 for (i, n) in voice.iter().enumerate() {
5009 total += n.beats();
5010 if total > max_beats + 1e-9 {
5011 cutoff = i;
5012 break;
5013 }
5014 }
5015 voice.truncate(cutoff);
5016 pad_voice_to_measure(voice, max_beats);
5017}
5018
5019fn pad_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
5020 let mut used = crate::voice_duration_beats(voice, max_beats);
5021 while max_beats - used > 1e-9 {
5022 let remaining = max_beats - used;
5023 let rest = Note::rest(Duration::whole_filling_beats(remaining));
5024 used += rest.beats();
5025 voice.push(rest);
5026 }
5027}
5028
5029#[cfg(test)]
5030mod tests {
5031 use super::*;
5032 use crate::ScoreEngine;
5033 use crate::model::pitch::Step;
5034
5035 fn default_engine_score() -> Score {
5036 let mut s = Score::default();
5037 for part in &mut s.parts {
5038 for staff in &mut part.staves {
5039 for (i, m) in staff.measures.iter_mut().enumerate() {
5040 m.number = i as u32 + 1;
5041 }
5042 }
5043 }
5044 s
5045 }
5046
5047 #[test]
5048 fn legacy_scale_voice_range_command_defaults_to_preserve_tuplet_ratio() {
5049 let command: Command = serde_json::from_str(
5050 r#"{"type":"scale_voice_range","part_index":0,"staff_index":0,"voice":0,"start_measure":0,"end_measure":0,"scale":"half"}"#,
5051 )
5052 .expect("legacy scale command deserializes");
5053 let Command::ScaleVoiceRange(command) = command else {
5054 panic!("expected scale command");
5055 };
5056 assert_eq!(command.tuplet_policy, TupletScalePolicy::PreserveRatio);
5057 }
5058
5059 fn score_with_typed_spanner() -> Score {
5060 use crate::model::score::{NotationSpanner, NotationSpannerKind};
5061
5062 let mut score = Score::new("Spanner edits", 120, 4, 4, 0, 1);
5063 score.parts[0].staves[0].measures[0].voices[0] = (0..4)
5064 .map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
5065 .collect();
5066 score.spanners.push(NotationSpanner {
5067 id: "typed-glissando".to_string(),
5068 kind: NotationSpannerKind::Glissando,
5069 start: NoteAddr {
5070 part: 0,
5071 staff: 0,
5072 measure: 0,
5073 voice: 0,
5074 note: 1,
5075 },
5076 end: NoteAddr {
5077 part: 0,
5078 staff: 0,
5079 measure: 0,
5080 voice: 0,
5081 note: 2,
5082 },
5083 number: Some(1),
5084 line_type: None,
5085 text: None,
5086 placement: None,
5087 ottava_size: None,
5088 ottava_type: None,
5089 });
5090 score
5091 }
5092
5093 fn insert_note_at(position: usize) -> Command {
5094 Command::AddNote(AddNoteCmd {
5095 part_index: 0,
5096 staff_index: 0,
5097 measure_index: 0,
5098 voice: 0,
5099 position,
5100 pitch: Some(Pitch::new(Step::D, 5)),
5101 duration: Duration::Quarter,
5102 dot_count: 0,
5103 is_rest: false,
5104 tuplet: None,
5105 })
5106 }
5107
5108 #[test]
5109 fn add_note_inserts_into_voice() {
5110 let mut score = default_engine_score();
5111 let cmd = Command::AddNote(AddNoteCmd {
5112 part_index: 0,
5113 staff_index: 0,
5114 measure_index: 0,
5115 voice: 0,
5116 position: 0,
5117 pitch: Some(Pitch::new(Step::C, 4)),
5118 duration: Duration::Quarter,
5119 dot_count: 0,
5120 is_rest: false,
5121 tuplet: None,
5122 });
5123 apply_command(&cmd, &mut score).unwrap();
5124 let first = &score.parts[0].staves[0].measures[0].voices[0][0];
5125 assert!(!first.is_rest);
5126 assert_eq!(first.pitches[0].step, Step::C);
5127 }
5128
5129 #[test]
5130 fn typed_spanners_follow_note_insertions_at_every_relative_position() {
5131 for (position, expected_start, expected_end) in [
5132 (0, 2, 3), (1, 2, 3), (2, 1, 3), (4, 1, 2), (3, 1, 2), ] {
5138 let mut score = score_with_typed_spanner();
5139 apply_command(&insert_note_at(position), &mut score).unwrap();
5140 let span = score.spanners.first().expect("span remains attached");
5141 assert_eq!(span.start.note, expected_start, "position {position}");
5142 assert_eq!(span.end.note, expected_end, "position {position}");
5143 }
5144 }
5145
5146 #[test]
5147 fn deleting_a_typed_spanner_endpoint_removes_the_whole_span_and_undo_redo_is_atomic() {
5148 let mut score = score_with_typed_spanner();
5149 let endpoint_id = score.parts[0].staves[0].measures[0].voices[0][1].id.clone();
5150 let mut stack = CommandStack::new(8);
5151
5152 stack
5153 .execute(
5154 Command::DeleteNote(DeleteNoteCmd {
5155 note_id: endpoint_id,
5156 part_index: 0,
5157 staff_index: 0,
5158 measure_index: 0,
5159 voice: 0,
5160 }),
5161 &mut score,
5162 )
5163 .unwrap();
5164 assert!(score.spanners.is_empty());
5165
5166 stack.undo(&mut score).unwrap();
5167 assert_eq!(score.spanners[0].start.note, 1);
5168 assert_eq!(score.spanners[0].end.note, 2);
5169
5170 stack.redo(&mut score).unwrap();
5171 assert!(score.spanners.is_empty());
5172 }
5173
5174 #[test]
5175 fn batched_structural_edits_remap_typed_spanners_and_restore_them_on_undo() {
5176 let mut score = score_with_typed_spanner();
5177 let mut stack = CommandStack::new(8);
5178 stack
5179 .batch_execute(
5180 vec![
5181 insert_note_at(0),
5182 Command::SetTempo(SetTempoCmd { bpm: 144 }),
5183 ],
5184 &mut score,
5185 )
5186 .unwrap();
5187 assert_eq!(score.spanners[0].start.note, 2);
5188 assert_eq!(score.spanners[0].end.note, 3);
5189
5190 stack.undo(&mut score).unwrap();
5191 assert_eq!(score.spanners[0].start.note, 1);
5192 assert_eq!(score.spanners[0].end.note, 2);
5193
5194 stack.redo(&mut score).unwrap();
5195 assert_eq!(score.spanners[0].start.note, 2);
5196 assert_eq!(score.spanners[0].end.note, 3);
5197 }
5198
5199 #[test]
5200 fn typed_spanners_remap_only_the_edited_endpoint_across_staff_and_voice() {
5201 use crate::model::score::{NotationSpanner, NotationSpannerKind, ScoreTemplate};
5202
5203 let mut score = Score::template(ScoreTemplate::Piano);
5204 for staff in &mut score.parts[0].staves {
5205 let notes: Vec<Note> = (0..4)
5206 .map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
5207 .collect();
5208 staff.measures[0].voices[0] = notes.clone();
5209 staff.measures[0].voices[1] = notes;
5210 }
5211 score.spanners.push(NotationSpanner {
5212 id: "cross-staff".to_string(),
5213 kind: NotationSpannerKind::Slur,
5214 start: NoteAddr {
5215 part: 0,
5216 staff: 0,
5217 measure: 0,
5218 voice: 0,
5219 note: 1,
5220 },
5221 end: NoteAddr {
5222 part: 0,
5223 staff: 1,
5224 measure: 0,
5225 voice: 1,
5226 note: 2,
5227 },
5228 number: Some(2),
5229 line_type: None,
5230 text: None,
5231 placement: None,
5232 ottava_size: None,
5233 ottava_type: None,
5234 });
5235
5236 let mut stack = CommandStack::new(8);
5237 stack.execute(insert_note_at(0), &mut score).unwrap();
5238 let span = &score.spanners[0];
5239 assert_eq!(span.start.note, 2);
5240 assert_eq!(span.end.staff, 1);
5241 assert_eq!(span.end.voice, 1);
5242 assert_eq!(span.end.note, 2);
5243 }
5244
5245 #[test]
5246 fn set_fingerings_keeps_first_legacy_value_in_sync() {
5247 let mut score = default_engine_score();
5248 apply_command(
5249 &Command::AddNote(AddNoteCmd {
5250 part_index: 0,
5251 staff_index: 0,
5252 measure_index: 0,
5253 voice: 0,
5254 position: 0,
5255 pitch: Some(Pitch::new(Step::C, 4)),
5256 duration: Duration::Quarter,
5257 dot_count: 0,
5258 is_rest: false,
5259 tuplet: None,
5260 }),
5261 &mut score,
5262 )
5263 .unwrap();
5264 apply_command(
5265 &Command::SetFingerings(SetFingeringsCmd {
5266 part_index: 0,
5267 staff_index: 0,
5268 measure_index: 0,
5269 voice: 0,
5270 note_index: 0,
5271 fingerings: vec![1, 3, 4],
5272 }),
5273 &mut score,
5274 )
5275 .unwrap();
5276 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
5277 assert_eq!(note.fingerings, vec![1, 3, 4]);
5278 assert_eq!(note.fingering, Some(1));
5279 }
5280
5281 #[test]
5282 fn set_figured_bass_replaces_measure_figures() {
5283 let mut score = default_engine_score();
5284 let figures = vec![FiguredBassFigure {
5285 number: "6".to_string(),
5286 alter: Some("-1".to_string()),
5287 prefix: Some("+".to_string()),
5288 suffix: None,
5289 extender: false,
5290 }];
5291 apply_command(
5292 &Command::SetFiguredBass(SetFiguredBassCmd {
5293 measure_index: 0,
5294 figures: figures.clone(),
5295 }),
5296 &mut score,
5297 )
5298 .unwrap();
5299 assert_eq!(score.parts[0].staves[0].measures[0].figured_bass, figures);
5300 }
5301
5302 #[test]
5303 fn set_measure_text_supports_append_replace_remove_and_undo_redo() {
5304 let mut engine = crate::ScoreEngine::new();
5305 let address = SetMeasureTextCmd {
5306 part_index: 0,
5307 staff_index: 0,
5308 measure_index: 0,
5309 text_index: 0,
5310 text: Some(StyledText {
5311 style: crate::TextStyle::Technique,
5312 text: "dolce".to_string(),
5313 placement: None,
5314 offset_x: None,
5315 offset_y: None,
5316 relative_x: None,
5317 relative_y: None,
5318 }),
5319 };
5320 engine.apply(Command::SetMeasureText(address)).unwrap();
5321 assert_eq!(
5322 engine.score.parts[0].staves[0].measures[0].texts[0],
5323 StyledText {
5324 style: crate::TextStyle::Technique,
5325 text: "dolce".to_string(),
5326 placement: None,
5327 offset_x: None,
5328 offset_y: None,
5329 relative_x: None,
5330 relative_y: None,
5331 }
5332 );
5333
5334 engine
5335 .apply(Command::SetMeasureText(SetMeasureTextCmd {
5336 text_index: 0,
5337 text: Some(StyledText {
5338 style: crate::TextStyle::RehearsalMark,
5339 text: "A".to_string(),
5340 placement: None,
5341 offset_x: None,
5342 offset_y: None,
5343 relative_x: None,
5344 relative_y: None,
5345 }),
5346 ..SetMeasureTextCmd {
5347 part_index: 0,
5348 staff_index: 0,
5349 measure_index: 0,
5350 text_index: 0,
5351 text: None,
5352 }
5353 }))
5354 .unwrap();
5355 assert_eq!(
5356 engine.score.parts[0].staves[0].measures[0].texts[0].style,
5357 crate::TextStyle::RehearsalMark
5358 );
5359
5360 engine
5361 .apply(Command::SetMeasureText(SetMeasureTextCmd {
5362 text_index: 0,
5363 text: None,
5364 ..SetMeasureTextCmd {
5365 part_index: 0,
5366 staff_index: 0,
5367 measure_index: 0,
5368 text_index: 0,
5369 text: None,
5370 }
5371 }))
5372 .unwrap();
5373 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
5374 engine.undo().unwrap();
5375 assert_eq!(engine.score.parts[0].staves[0].measures[0].texts.len(), 1);
5376 engine.redo().unwrap();
5377 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
5378 }
5379
5380 #[test]
5381 fn set_measure_text_rejects_invalid_index_atomically_and_round_trips_json() {
5382 let mut score = default_engine_score();
5383 let before = score.clone();
5384 let command = Command::SetMeasureText(SetMeasureTextCmd {
5385 part_index: 0,
5386 staff_index: 0,
5387 measure_index: 0,
5388 text_index: 2,
5389 text: Some(StyledText {
5390 style: crate::TextStyle::Expression,
5391 text: "espressivo".to_string(),
5392 placement: None,
5393 offset_x: None,
5394 offset_y: None,
5395 relative_x: None,
5396 relative_y: None,
5397 }),
5398 });
5399 let json = serde_json::to_string(&command).unwrap();
5400 let restored: Command = serde_json::from_str(&json).unwrap();
5401 assert_eq!(
5402 serde_json::to_value(&restored).unwrap(),
5403 serde_json::to_value(&command).unwrap()
5404 );
5405 assert!(apply_command(&restored, &mut score).is_err());
5406 assert_eq!(
5407 serde_json::to_value(&score).unwrap(),
5408 serde_json::to_value(&before).unwrap()
5409 );
5410 }
5411
5412 #[test]
5413 fn set_score_text_supports_append_replace_remove_and_undo_redo() {
5414 let mut engine = crate::ScoreEngine::new();
5415 let text = StyledText {
5416 style: crate::TextStyle::Expression,
5417 text: "Title".to_string(),
5418 placement: None,
5419 offset_x: None,
5420 offset_y: None,
5421 relative_x: None,
5422 relative_y: None,
5423 };
5424 engine
5425 .apply(Command::SetScoreText(SetScoreTextCmd {
5426 text_index: 0,
5427 text: Some(text.clone()),
5428 }))
5429 .unwrap();
5430 assert_eq!(engine.score.texts, vec![text.clone()]);
5431
5432 let mut replacement = text.clone();
5433 replacement.text = "Subtitle".to_string();
5434 engine
5435 .apply(Command::SetScoreText(SetScoreTextCmd {
5436 text_index: 0,
5437 text: Some(replacement.clone()),
5438 }))
5439 .unwrap();
5440 assert_eq!(engine.score.texts, vec![replacement]);
5441
5442 engine
5443 .apply(Command::SetScoreText(SetScoreTextCmd {
5444 text_index: 0,
5445 text: None,
5446 }))
5447 .unwrap();
5448 assert!(engine.score.texts.is_empty());
5449 engine.undo().unwrap();
5450 assert_eq!(engine.score.texts.len(), 1);
5451 engine.redo().unwrap();
5452 assert!(engine.score.texts.is_empty());
5453 }
5454
5455 #[test]
5456 fn set_score_style_overrides_is_undoable_and_rejects_invalid_values() {
5457 let mut engine = crate::ScoreEngine::new();
5458 let overrides = vec![ViewStyleOverride {
5459 property: super::super::score::ViewStyleProperty::SystemGap,
5460 value: 2.5,
5461 }];
5462 engine
5463 .apply(Command::SetScoreStyleOverrides(SetScoreStyleOverridesCmd {
5464 overrides: overrides.clone(),
5465 }))
5466 .unwrap();
5467 assert_eq!(engine.score.style_overrides, overrides);
5468 engine.undo().unwrap();
5469 assert!(engine.score.style_overrides.is_empty());
5470 engine.redo().unwrap();
5471 assert_eq!(engine.score.style_overrides, overrides);
5472
5473 let before = engine.score.clone();
5474 assert!(
5475 engine
5476 .apply(Command::SetScoreStyleOverrides(SetScoreStyleOverridesCmd {
5477 overrides: vec![ViewStyleOverride {
5478 property: super::super::score::ViewStyleProperty::TextScale,
5479 value: f32::NAN,
5480 }],
5481 }))
5482 .is_err()
5483 );
5484 assert_eq!(
5485 serde_json::to_value(&engine.score).unwrap(),
5486 serde_json::to_value(&before).unwrap()
5487 );
5488 }
5489
5490 #[test]
5491 fn set_object_style_overrides_is_undoable_and_requires_existing_target() {
5492 let mut engine = crate::ScoreEngine::new();
5493 engine.score.texts.push(StyledText {
5494 style: crate::TextStyle::Expression,
5495 text: "Allegro".into(),
5496 placement: None,
5497 offset_x: None,
5498 offset_y: None,
5499 relative_x: None,
5500 relative_y: None,
5501 });
5502 let overrides = vec![ObjectStyleOverride {
5503 target: super::super::score::ObjectStyleTarget::ScoreText { text_index: 0 },
5504 property: super::super::score::ViewStyleProperty::TextScale,
5505 value: 1.2,
5506 provenance: None,
5507 }];
5508 engine
5509 .apply(Command::SetObjectStyleOverrides(
5510 SetObjectStyleOverridesCmd {
5511 overrides: overrides.clone(),
5512 },
5513 ))
5514 .unwrap();
5515 assert_eq!(engine.score.object_style_overrides, overrides);
5516 engine.undo().unwrap();
5517 assert!(engine.score.object_style_overrides.is_empty());
5518
5519 assert!(
5520 engine
5521 .apply(Command::SetObjectStyleOverrides(
5522 SetObjectStyleOverridesCmd {
5523 overrides: vec![ObjectStyleOverride {
5524 target: super::super::score::ObjectStyleTarget::ScoreText {
5525 text_index: 1,
5526 },
5527 property: super::super::score::ViewStyleProperty::TextScale,
5528 value: 1.2,
5529 provenance: None,
5530 }],
5531 }
5532 ))
5533 .is_err()
5534 );
5535 }
5536
5537 #[test]
5538 fn set_harmony_range_is_undoable_and_json_compatible() {
5539 let mut score = default_engine_score();
5540 score.parts[0].staves[0].measures[0].voices[0] =
5541 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
5542 score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
5543 root: "C".to_owned(),
5544 kind: "major".to_owned(),
5545 bass: None,
5546 placement: None,
5547 extender: true,
5548 harmonic_degree: None,
5549 harmony_function: None,
5550 harmony_type: None,
5551 chord_ref: None,
5552 range_end: None,
5553 degrees: Vec::new(),
5554 });
5555 let mut engine = ScoreEngine::new();
5556 engine.replace_score(score);
5557 let command = Command::SetHarmonyRange(SetHarmonyRangeCmd {
5558 part_index: 0,
5559 staff_index: 0,
5560 measure_index: 0,
5561 voice: 0,
5562 note_index: 0,
5563 end: Some(NoteAddr {
5564 part: 0,
5565 staff: 0,
5566 measure: 0,
5567 voice: 0,
5568 note: 0,
5569 }),
5570 });
5571 let json = serde_json::to_string(&command).unwrap();
5572 let restored: Command = serde_json::from_str(&json).unwrap();
5573 engine.apply(restored).unwrap();
5574 assert!(
5575 engine.score.parts[0].staves[0].measures[0].voices[0][0]
5576 .chord_symbol
5577 .as_ref()
5578 .and_then(|chord| chord.range_end.as_ref())
5579 .is_some()
5580 );
5581 engine.undo().unwrap();
5582 assert!(
5583 engine.score.parts[0].staves[0].measures[0].voices[0][0]
5584 .chord_symbol
5585 .as_ref()
5586 .is_some_and(|chord| chord.range_end.is_none())
5587 );
5588 }
5589
5590 #[test]
5591 fn set_harp_pedal_diagrams_is_undoable_and_json_compatible() {
5592 let mut engine = ScoreEngine::new();
5593 let mut diagram = HarpPedalDiagram::default();
5594 diagram.positions[0] = super::super::score::HarpPedalPosition::Flat;
5595 diagram.positions[6] = super::super::score::HarpPedalPosition::Sharp;
5596 let command = Command::SetHarpPedalDiagrams(SetHarpPedalDiagramsCmd {
5597 part_index: 0,
5598 staff_index: 0,
5599 measure_index: 0,
5600 diagrams: vec![diagram.clone()],
5601 });
5602 let restored: Command =
5603 serde_json::from_str(&serde_json::to_string(&command).unwrap()).unwrap();
5604 engine.apply(restored).unwrap();
5605 assert_eq!(
5606 engine.score.parts[0].staves[0].measures[0].harp_pedal_diagrams,
5607 vec![diagram]
5608 );
5609 engine.undo().unwrap();
5610 assert!(
5611 engine.score.parts[0].staves[0].measures[0]
5612 .harp_pedal_diagrams
5613 .is_empty()
5614 );
5615 }
5616
5617 #[test]
5618 fn set_note_placement_is_undoable_and_rejects_non_finite_values() {
5619 let mut score = default_engine_score();
5620 score.parts[0].staves[0].measures[0].voices[0] =
5621 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5622 let mut engine = ScoreEngine::new();
5623 engine.replace_score(score);
5624 let command = Command::SetNotePlacement(SetNotePlacementCmd {
5625 part_index: 0,
5626 staff_index: 0,
5627 measure_index: 0,
5628 voice: 0,
5629 note_index: 0,
5630 offset_x: Some(12.5),
5631 offset_y: Some(-3.0),
5632 relative_x: Some(1.25),
5633 relative_y: Some(-0.5),
5634 });
5635 engine.apply(command).unwrap();
5636 let note = &engine.score.parts[0].staves[0].measures[0].voices[0][0];
5637 assert_eq!(note.offset_x, Some(12.5));
5638 assert_eq!(note.relative_y, Some(-0.5));
5639 engine.undo().unwrap();
5640 assert_eq!(
5641 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
5642 None
5643 );
5644 let invalid = Command::SetNotePlacement(SetNotePlacementCmd {
5645 part_index: 0,
5646 staff_index: 0,
5647 measure_index: 0,
5648 voice: 0,
5649 note_index: 0,
5650 offset_x: Some(f64::NAN),
5651 offset_y: None,
5652 relative_x: None,
5653 relative_y: None,
5654 });
5655 assert!(engine.apply(invalid).is_err());
5656 assert_eq!(
5657 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
5658 None
5659 );
5660 }
5661
5662 #[test]
5663 fn set_guitar_bend_alter_updates_note_and_can_clear() {
5664 let mut score = default_engine_score();
5665 apply_command(
5666 &Command::AddNote(AddNoteCmd {
5667 part_index: 0,
5668 staff_index: 0,
5669 measure_index: 0,
5670 voice: 0,
5671 position: 0,
5672 pitch: Some(Pitch::new(Step::G, 4)),
5673 duration: Duration::Quarter,
5674 dot_count: 0,
5675 is_rest: false,
5676 tuplet: None,
5677 }),
5678 &mut score,
5679 )
5680 .unwrap();
5681 let location = SetGuitarBendAlterCmd {
5682 part_index: 0,
5683 staff_index: 0,
5684 measure_index: 0,
5685 voice: 0,
5686 note_index: 0,
5687 alter_cents: Some(200),
5688 };
5689 apply_command(&Command::SetGuitarBendAlter(location.clone()), &mut score).unwrap();
5690 assert_eq!(
5691 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
5692 Some(200)
5693 );
5694 let mut cleared = location;
5695 cleared.alter_cents = None;
5696 apply_command(&Command::SetGuitarBendAlter(cleared), &mut score).unwrap();
5697 assert_eq!(
5698 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
5699 None
5700 );
5701 }
5702
5703 #[test]
5704 fn set_guitar_bend_curve_is_undoable_and_json_compatible() {
5705 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5706 let command = Command::SetGuitarBendCurve(SetGuitarBendCurveCmd {
5707 part_index: 0,
5708 staff_index: 0,
5709 measure_index: 0,
5710 voice: 0,
5711 note_index: 0,
5712 points: vec![
5713 crate::GuitarBendPoint {
5714 position_per_mille: 0,
5715 alter_cents: 0,
5716 },
5717 crate::GuitarBendPoint {
5718 position_per_mille: 500,
5719 alter_cents: 200,
5720 },
5721 crate::GuitarBendPoint {
5722 position_per_mille: 1000,
5723 alter_cents: 0,
5724 },
5725 ],
5726 });
5727 let decoded: Command =
5728 serde_json::from_str(&serde_json::to_string(&command).unwrap()).unwrap();
5729 let mut stack = CommandStack::new(16);
5730 stack.execute(decoded, &mut score).unwrap();
5731 assert_eq!(
5732 score.parts[0].staves[0].measures[0].voices[0][0]
5733 .guitar_bend_curve
5734 .len(),
5735 3
5736 );
5737 stack.undo(&mut score).unwrap();
5738 assert!(
5739 score.parts[0].staves[0].measures[0].voices[0][0]
5740 .guitar_bend_curve
5741 .is_empty()
5742 );
5743 }
5744
5745 #[test]
5746 fn set_duration_updates_note_and_preserves_measure_capacity() {
5747 let mut score = default_engine_score();
5748 apply_command(
5749 &Command::AddNote(AddNoteCmd {
5750 part_index: 0,
5751 staff_index: 0,
5752 measure_index: 0,
5753 voice: 0,
5754 position: 0,
5755 pitch: Some(Pitch::new(Step::C, 4)),
5756 duration: Duration::Quarter,
5757 dot_count: 0,
5758 is_rest: false,
5759 tuplet: None,
5760 }),
5761 &mut score,
5762 )
5763 .unwrap();
5764 apply_command(
5765 &Command::SetDuration(SetDurationCmd {
5766 part_index: 0,
5767 staff_index: 0,
5768 measure_index: 0,
5769 voice: 0,
5770 note_index: 0,
5771 duration: Duration::Half,
5772 dot_count: 1,
5773 }),
5774 &mut score,
5775 )
5776 .unwrap();
5777 let voice = &score.parts[0].staves[0].measures[0].voices[0];
5778 assert_eq!(voice[0].duration, Duration::Half);
5779 assert_eq!(voice[0].dot_count, 1);
5780 assert!((voice.iter().map(|note| note.beats()).sum::<f64>() - 4.0).abs() < 1e-9);
5781 }
5782
5783 #[test]
5784 fn set_tempo_updates_score() {
5785 let mut score = default_engine_score();
5786 let cmd = Command::SetTempo(SetTempoCmd { bpm: 160 });
5787 apply_command(&cmd, &mut score).unwrap();
5788 assert_eq!(score.settings.tempo_bpm, 160);
5789 }
5790
5791 #[test]
5792 fn add_measure_increases_count() {
5793 let mut score = default_engine_score();
5794 let before = score.measure_count();
5795 apply_command(
5796 &Command::AddMeasure(AddMeasureCmd { after_index: 0 }),
5797 &mut score,
5798 )
5799 .unwrap();
5800 assert_eq!(score.measure_count(), before + 1);
5801 }
5802
5803 #[test]
5804 fn delete_measure_decreases_count() {
5805 let mut score = default_engine_score();
5806 let before = score.measure_count();
5807 apply_command(
5808 &Command::DeleteMeasure(DeleteMeasureCmd { measure_index: 0 }),
5809 &mut score,
5810 )
5811 .unwrap();
5812 assert_eq!(score.measure_count(), before - 1);
5813 }
5814
5815 #[test]
5816 fn undo_restores_score() {
5817 let mut stack = CommandStack::new(50);
5818 let mut score = default_engine_score();
5819 let before = score.settings.tempo_bpm;
5820 stack
5821 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
5822 .unwrap();
5823 assert_eq!(score.settings.tempo_bpm, 200);
5824 stack.undo(&mut score).unwrap();
5825 assert_eq!(score.settings.tempo_bpm, before);
5826 }
5827
5828 #[test]
5829 fn redo_reapplies_command() {
5830 let mut stack = CommandStack::new(50);
5831 let mut score = default_engine_score();
5832 stack
5833 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
5834 .unwrap();
5835 stack.undo(&mut score).unwrap();
5836 stack.redo(&mut score).unwrap();
5837 assert_eq!(score.settings.tempo_bpm, 200);
5838 }
5839
5840 #[test]
5841 fn undo_nothing_returns_error() {
5842 let mut stack = CommandStack::new(50);
5843 let mut score = default_engine_score();
5844 assert!(stack.undo(&mut score).is_err());
5845 }
5846
5847 #[test]
5848 fn add_part_appends_part() {
5849 let mut score = default_engine_score();
5850 let before = score.parts.len();
5851 apply_command(
5852 &Command::AddPart(AddPartCmd {
5853 name: "Violin".into(),
5854 short_name: "Vln.".into(),
5855 clefs: vec!["Treble".into()],
5856 midi_channel: 0,
5857 midi_program: 0,
5858 }),
5859 &mut score,
5860 )
5861 .unwrap();
5862 assert_eq!(score.parts.len(), before + 1);
5863 }
5864
5865 #[test]
5866 fn delete_part_removes_part() {
5867 let mut score = default_engine_score();
5868 apply_command(
5869 &Command::AddPart(AddPartCmd {
5870 name: "Violin".into(),
5871 short_name: "V.".into(),
5872 clefs: vec!["Treble".into()],
5873 midi_channel: 0,
5874 midi_program: 0,
5875 }),
5876 &mut score,
5877 )
5878 .unwrap();
5879 let before = score.parts.len();
5880 apply_command(
5881 &Command::DeletePart(DeletePartCmd { part_index: 0 }),
5882 &mut score,
5883 )
5884 .unwrap();
5885 assert_eq!(score.parts.len(), before - 1);
5886 }
5887
5888 #[test]
5889 fn delete_part_out_of_range_returns_err() {
5890 let mut score = default_engine_score();
5891 assert!(
5892 apply_command(
5893 &Command::DeletePart(DeletePartCmd { part_index: 99 }),
5894 &mut score
5895 )
5896 .is_err()
5897 );
5898 }
5899
5900 #[test]
5901 fn delete_part_undo_restores_part() {
5902 let mut stack = CommandStack::new(50);
5903 let mut score = default_engine_score();
5904 apply_command(
5905 &Command::AddPart(AddPartCmd {
5906 name: "Violin".into(),
5907 short_name: "V.".into(),
5908 clefs: vec!["Treble".into()],
5909 midi_channel: 0,
5910 midi_program: 0,
5911 }),
5912 &mut score,
5913 )
5914 .unwrap();
5915 let before = score.parts.len();
5916 stack
5917 .execute(
5918 Command::DeletePart(DeletePartCmd { part_index: 0 }),
5919 &mut score,
5920 )
5921 .unwrap();
5922 assert_eq!(score.parts.len(), before - 1);
5923 stack.undo(&mut score).unwrap();
5924 assert_eq!(score.parts.len(), before);
5925 }
5926
5927 #[test]
5928 fn reorder_parts_remaps_linked_views_and_preserves_undo() {
5929 let mut engine = crate::ScoreEngine::new();
5930 engine
5931 .apply(Command::AddPart(AddPartCmd {
5932 name: "Flute".into(),
5933 short_name: "Fl.".into(),
5934 clefs: vec!["Treble".into()],
5935 midi_channel: 1,
5936 midi_program: 73,
5937 }))
5938 .unwrap();
5939 engine
5940 .score
5941 .views
5942 .push(ScoreView::linked_part("flute", "Flute", 1));
5943 engine.score.part_groups.push(PartGroup {
5944 first_part: 0,
5945 last_part: 1,
5946 symbol: super::super::score::PartGroupSymbol::Bracket,
5947 barlines_connect: false,
5948 });
5949
5950 engine
5951 .apply(Command::ReorderParts(ReorderPartsCmd { order: vec![1, 0] }))
5952 .unwrap();
5953 assert_eq!(engine.score.parts[0].name, "Flute");
5954 assert_eq!(engine.score.views[0].parts, vec![0]);
5955 engine.undo().unwrap();
5956 assert_eq!(engine.score.parts[0].name, "Piano");
5957 assert_eq!(engine.score.views[0].parts, vec![1]);
5958 }
5959
5960 #[test]
5961 fn reorder_parts_rejects_splitting_a_part_group_without_mutation() {
5962 let mut score = Score::template(ScoreTemplate::StringQuartet);
5963 score.part_groups.push(PartGroup {
5964 first_part: 0,
5965 last_part: 1,
5966 symbol: super::super::score::PartGroupSymbol::Bracket,
5967 barlines_connect: false,
5968 });
5969 let before = serde_json::to_value(&score).unwrap();
5970
5971 assert!(
5972 apply_command(
5973 &Command::ReorderParts(ReorderPartsCmd {
5974 order: vec![0, 2, 1, 3],
5975 }),
5976 &mut score,
5977 )
5978 .is_err()
5979 );
5980 assert_eq!(serde_json::to_value(&score).unwrap(), before);
5981 }
5982
5983 #[test]
5984 fn set_metadata_updates_title() {
5985 let mut score = default_engine_score();
5986 apply_command(
5987 &Command::SetMetadata(SetMetadataCmd {
5988 title: Some("New Title".into()),
5989 ..Default::default()
5990 }),
5991 &mut score,
5992 )
5993 .unwrap();
5994 assert_eq!(score.metadata.title, "New Title");
5995 }
5996
5997 #[test]
5998 fn set_metadata_none_fields_skipped() {
5999 let mut score = default_engine_score();
6000 let original_composer = score.metadata.composer.clone();
6001 apply_command(
6002 &Command::SetMetadata(SetMetadataCmd {
6003 title: Some("X".into()),
6004 ..Default::default()
6005 }),
6006 &mut score,
6007 )
6008 .unwrap();
6009 assert_eq!(score.metadata.composer, original_composer);
6010 }
6011
6012 #[test]
6013 fn set_volta_sets_bracket() {
6014 use crate::model::score::VoltaBracket;
6015 let mut score = default_engine_score();
6016 let volta = VoltaBracket {
6017 number: 1,
6018 kind: "begin_end".into(),
6019 numbers: Vec::new(),
6020 };
6021 apply_command(
6022 &Command::SetVolta(SetVoltaCmd {
6023 measure_index: 0,
6024 volta: Some(volta.clone()),
6025 }),
6026 &mut score,
6027 )
6028 .unwrap();
6029 assert!(score.parts[0].staves[0].measures[0].volta.is_some());
6030 }
6031
6032 #[test]
6033 fn set_volta_none_clears_bracket() {
6034 use crate::model::score::VoltaBracket;
6035 let mut score = default_engine_score();
6036 score.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
6037 number: 1,
6038 kind: "begin_end".into(),
6039 numbers: Vec::new(),
6040 });
6041 apply_command(
6042 &Command::SetVolta(SetVoltaCmd {
6043 measure_index: 0,
6044 volta: None,
6045 }),
6046 &mut score,
6047 )
6048 .unwrap();
6049 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
6050 }
6051
6052 #[test]
6053 fn set_volta_undo_restores_old() {
6054 use crate::model::score::VoltaBracket;
6055 let mut stack = CommandStack::new(50);
6056 let mut score = default_engine_score();
6057 stack
6058 .execute(
6059 Command::SetVolta(SetVoltaCmd {
6060 measure_index: 0,
6061 volta: Some(VoltaBracket {
6062 number: 1,
6063 kind: "begin_end".into(),
6064 numbers: Vec::new(),
6065 }),
6066 }),
6067 &mut score,
6068 )
6069 .unwrap();
6070 stack.undo(&mut score).unwrap();
6071 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
6072 }
6073
6074 #[test]
6075 fn set_clef_updates_staff_clef() {
6076 use crate::model::notation::Clef;
6077 let mut score = default_engine_score();
6078 apply_command(
6079 &Command::SetClef(SetClefCmd {
6080 part_index: 0,
6081 staff_index: 0,
6082 clef: Clef::Bass,
6083 }),
6084 &mut score,
6085 )
6086 .unwrap();
6087 assert_eq!(score.parts[0].staves[0].clef, Clef::Bass);
6088 }
6089
6090 #[test]
6091 fn set_clef_out_of_range_returns_err() {
6092 use crate::model::notation::Clef;
6093 let mut score = default_engine_score();
6094 assert!(
6095 apply_command(
6096 &Command::SetClef(SetClefCmd {
6097 part_index: 99,
6098 staff_index: 0,
6099 clef: Clef::Bass,
6100 }),
6101 &mut score
6102 )
6103 .is_err()
6104 );
6105 }
6106
6107 #[test]
6108 fn set_part_name_updates_name() {
6109 let mut score = default_engine_score();
6110 apply_command(
6111 &Command::SetPartName(SetPartNameCmd {
6112 part_index: 0,
6113 name: "Violin".into(),
6114 short_name: "Vln.".into(),
6115 }),
6116 &mut score,
6117 )
6118 .unwrap();
6119 assert_eq!(score.parts[0].name, "Violin");
6120 assert_eq!(score.parts[0].short_name, "Vln.");
6121 }
6122
6123 #[test]
6124 fn set_part_name_undo_restores_old() {
6125 let mut stack = CommandStack::new(50);
6126 let mut score = default_engine_score();
6127 let original = score.parts[0].name.clone();
6128 stack
6129 .execute(
6130 Command::SetPartName(SetPartNameCmd {
6131 part_index: 0,
6132 name: "Flute".into(),
6133 short_name: "Fl.".into(),
6134 }),
6135 &mut score,
6136 )
6137 .unwrap();
6138 stack.undo(&mut score).unwrap();
6139 assert_eq!(score.parts[0].name, original);
6140 }
6141
6142 #[test]
6143 fn set_metadata_undo_restores_old_title() {
6144 let mut stack = CommandStack::new(50);
6145 let mut score = default_engine_score();
6146 let original = score.metadata.title.clone();
6147 stack
6148 .execute(
6149 Command::SetMetadata(SetMetadataCmd {
6150 title: Some("Changed".into()),
6151 ..Default::default()
6152 }),
6153 &mut score,
6154 )
6155 .unwrap();
6156 assert_ne!(score.metadata.title, original);
6157 stack.undo(&mut score).unwrap();
6158 assert_eq!(score.metadata.title, original);
6159 }
6160
6161 #[test]
6162 fn set_midi_instrument_updates_channel_and_program() {
6163 let mut score = default_engine_score();
6164 apply_command(
6165 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
6166 part_index: 0,
6167 midi_channel: 2,
6168 midi_program: 40,
6169 }),
6170 &mut score,
6171 )
6172 .unwrap();
6173 assert_eq!(score.parts[0].midi_channel, 2);
6174 assert_eq!(score.parts[0].midi_program, 40);
6175 }
6176
6177 #[test]
6178 fn set_midi_instrument_clamps_channel_to_15() {
6179 let mut score = default_engine_score();
6180 apply_command(
6181 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
6182 part_index: 0,
6183 midi_channel: 20,
6184 midi_program: 0,
6185 }),
6186 &mut score,
6187 )
6188 .unwrap();
6189 assert_eq!(score.parts[0].midi_channel, 15);
6190 }
6191
6192 #[test]
6193 fn set_midi_instrument_undo_restores_old() {
6194 let mut stack = CommandStack::new(50);
6195 let mut score = default_engine_score();
6196 score.parts[0].midi_channel = 3;
6197 score.parts[0].midi_program = 10;
6198 stack
6199 .execute(
6200 Command::SetMidiInstrument(SetMidiInstrumentCmd {
6201 part_index: 0,
6202 midi_channel: 9,
6203 midi_program: 114,
6204 }),
6205 &mut score,
6206 )
6207 .unwrap();
6208 stack.undo(&mut score).unwrap();
6209 assert_eq!(score.parts[0].midi_channel, 3);
6210 assert_eq!(score.parts[0].midi_program, 10);
6211 }
6212
6213 #[test]
6214 fn set_transpose_updates_staff() {
6215 let mut score = default_engine_score();
6216 apply_command(
6217 &Command::SetTranspose(SetTransposeCmd {
6218 part_index: 0,
6219 staff_index: 0,
6220 semitones: -2,
6221 }),
6222 &mut score,
6223 )
6224 .unwrap();
6225 assert_eq!(score.parts[0].staves[0].transpose_semitones, -2);
6226 }
6227
6228 #[test]
6229 fn set_transpose_out_of_range_returns_err() {
6230 let mut score = default_engine_score();
6231 assert!(
6232 apply_command(
6233 &Command::SetTranspose(SetTransposeCmd {
6234 part_index: 99,
6235 staff_index: 0,
6236 semitones: -2,
6237 }),
6238 &mut score
6239 )
6240 .is_err()
6241 );
6242 }
6243
6244 #[test]
6245 fn transpose_staff_region_is_undoable_and_json_compatible() {
6246 let mut stack = CommandStack::new(50);
6247 let mut score = Score::new("Region", 120, 4, 4, 0, 2);
6248 for measure in &mut score.parts[0].staves[0].measures {
6249 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
6250 }
6251 let command = Command::TransposeStaffRegion(TransposeStaffRegionCmd {
6252 part_index: 0,
6253 staff_index: 0,
6254 start_measure: 1,
6255 end_measure: 2,
6256 semitones: 2,
6257 target: RegionalTranspositionTarget::Written,
6258 });
6259 let json = serde_json::to_string(&command).expect("command serializes");
6260 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6261 assert_eq!(command_key(&decoded), "TransposeStaffRegion");
6262 stack.execute(decoded, &mut score).expect("region applies");
6263 assert_eq!(
6264 score.parts[0].staves[0].measures[0].voices[0][0].pitches[0].to_midi(),
6265 60
6266 );
6267 assert_eq!(
6268 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0].to_midi(),
6269 62
6270 );
6271 stack.undo(&mut score).expect("region undo applies");
6272 assert_eq!(
6273 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0].to_midi(),
6274 60
6275 );
6276 }
6277
6278 #[test]
6279 fn set_tempo_at_measure_sets_tempo() {
6280 let mut score = default_engine_score();
6281 apply_command(
6282 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
6283 measure_index: 0,
6284 bpm: Some(80),
6285 }),
6286 &mut score,
6287 )
6288 .unwrap();
6289 assert_eq!(score.parts[0].staves[0].measures[0].tempo, Some(80));
6290 }
6291
6292 #[test]
6293 fn set_tempo_at_measure_none_clears_tempo() {
6294 let mut score = default_engine_score();
6295 score.parts[0].staves[0].measures[0].tempo = Some(120);
6296 apply_command(
6297 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
6298 measure_index: 0,
6299 bpm: None,
6300 }),
6301 &mut score,
6302 )
6303 .unwrap();
6304 assert!(score.parts[0].staves[0].measures[0].tempo.is_none());
6305 }
6306
6307 #[test]
6308 fn set_tempo_ramp_at_measure_applies() {
6309 let mut score = default_engine_score();
6310 apply_command(
6311 &Command::SetTempoRampAtMeasure(SetTempoRampAtMeasureCmd {
6312 measure_index: 0,
6313 target_bpm: Some(72),
6314 }),
6315 &mut score,
6316 )
6317 .expect("tempo ramp applies");
6318 assert_eq!(score.parts[0].staves[0].measures[0].tempo_ramp_to, Some(72));
6319 }
6320
6321 #[test]
6324 fn batch_execute_two_commands_single_undo() {
6325 let mut stack = CommandStack::new(50);
6326 let mut score = default_engine_score();
6327 let original_bpm = score.settings.tempo_bpm;
6328 stack
6329 .batch_execute(
6330 vec![
6331 Command::SetTempo(SetTempoCmd { bpm: 160 }),
6332 Command::SetTempo(SetTempoCmd { bpm: 180 }),
6333 ],
6334 &mut score,
6335 )
6336 .unwrap();
6337 assert_eq!(score.settings.tempo_bpm, 180);
6338 stack.undo(&mut score).unwrap();
6339 assert_eq!(score.settings.tempo_bpm, original_bpm);
6340 }
6341
6342 #[test]
6343 fn batch_execute_partial_failure_rollback() {
6344 let mut stack = CommandStack::new(50);
6345 let mut score = default_engine_score();
6346 let original_bpm = score.settings.tempo_bpm;
6347 let result = stack.batch_execute(
6348 vec![
6349 Command::SetTempo(SetTempoCmd { bpm: 160 }),
6350 Command::DeleteNote(DeleteNoteCmd {
6351 note_id: "nonexistent".into(),
6352 part_index: 99,
6353 staff_index: 0,
6354 measure_index: 0,
6355 voice: 0,
6356 }),
6357 ],
6358 &mut score,
6359 );
6360 assert!(result.is_err());
6361 assert_eq!(score.settings.tempo_bpm, original_bpm);
6362 }
6363
6364 #[test]
6365 fn execute_rejects_invalid_candidate_without_mutating_or_recording_history() {
6366 let mut stack = CommandStack::new(50);
6367 let mut score = default_engine_score();
6368 score.parts[0].staves[0].tablature = Some(crate::TablatureConfig {
6369 lines: 6,
6370 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6371 capo: 0,
6372 });
6373 stack
6374 .execute(
6375 Command::AddNote(AddNoteCmd {
6376 part_index: 0,
6377 staff_index: 0,
6378 measure_index: 0,
6379 voice: 0,
6380 position: 0,
6381 pitch: Some(Pitch::new(Step::C, 4)),
6382 duration: Duration::Quarter,
6383 dot_count: 0,
6384 is_rest: false,
6385 tuplet: None,
6386 }),
6387 &mut score,
6388 )
6389 .expect("valid pitched note should be accepted");
6390 let original = score.clone();
6391 let result = stack.execute(
6392 Command::SetTabPosition(SetTabPositionCmd {
6393 part_index: 0,
6394 staff_index: 0,
6395 measure_index: 0,
6396 voice: 0,
6397 note_index: 0,
6398 position: Some(crate::TabPosition { string: 7, fret: 0 }),
6399 }),
6400 &mut score,
6401 );
6402 assert!(matches!(result, Err(Error::InvalidScore)));
6403 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
6404 let original_note = &original.parts[0].staves[0].measures[0].voices[0][0];
6405 assert_eq!(note.pitches, original_note.pitches);
6406 assert_eq!(note.duration, original_note.duration);
6407 assert_eq!(note.tab_position, original_note.tab_position);
6408 assert_eq!(note.tab_positions, original_note.tab_positions);
6409 assert!(stack.can_undo());
6410 }
6411
6412 #[test]
6413 fn set_tablature_config_is_undoable_and_json_compatible() {
6414 let mut stack = CommandStack::new(50);
6415 let mut score = default_engine_score();
6416 let config = crate::TablatureConfig {
6417 lines: 6,
6418 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6419 capo: 2,
6420 };
6421 let command = Command::SetTablatureConfig(SetTablatureConfigCmd {
6422 part_index: 0,
6423 staff_index: 0,
6424 config: Some(config.clone()),
6425 });
6426 let json = serde_json::to_string(&command).expect("command should serialize");
6427 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6428 assert_eq!(command_key(&decoded), "SetTablatureConfig");
6429 stack
6430 .execute(decoded, &mut score)
6431 .expect("config should apply");
6432 assert_eq!(score.parts[0].staves[0].tablature, Some(config));
6433 assert_eq!(
6434 score.parts[0].staves[0].presentation.kind,
6435 StaffKind::Tablature
6436 );
6437 stack.undo(&mut score).expect("config undo should apply");
6438 assert!(score.parts[0].staves[0].tablature.is_none());
6439 assert_eq!(
6440 score.parts[0].staves[0].presentation.kind,
6441 StaffKind::Standard
6442 );
6443 stack.redo(&mut score).expect("config redo should apply");
6444 assert_eq!(
6445 score.parts[0].staves[0]
6446 .tablature
6447 .as_ref()
6448 .map(|tab| tab.capo),
6449 Some(2)
6450 );
6451 }
6452
6453 #[test]
6454 fn set_staff_presentation_is_undoable_and_requires_tablature_config() {
6455 let mut stack = CommandStack::new(50);
6456 let mut score = default_engine_score();
6457 let tab = StaffPresentation {
6458 kind: StaffKind::Tablature,
6459 lines: 6,
6460 line_distance: 1.25,
6461 small: true,
6462 cutaway: false,
6463 visible: true,
6464 notehead_scheme: super::super::score::StaffNoteheadScheme::Standard,
6465 tablature_rhythm_display: super::super::score::TablatureRhythmDisplay::FretOnly,
6466 tablature_fret_mark_style: super::super::score::TablatureFretMarkStyle::Arabic,
6467 };
6468 let rejected = Command::SetStaffPresentation(SetStaffPresentationCmd {
6469 part_index: 0,
6470 staff_index: 0,
6471 presentation: tab.clone(),
6472 });
6473 let before = score.clone();
6474 assert!(stack.execute(rejected, &mut score).is_err());
6475 assert_eq!(
6476 score.parts[0].staves[0].presentation,
6477 before.parts[0].staves[0].presentation
6478 );
6479 assert!(!stack.can_undo());
6480
6481 stack
6482 .execute(
6483 Command::SetTablatureConfig(SetTablatureConfigCmd {
6484 part_index: 0,
6485 staff_index: 0,
6486 config: Some(crate::TablatureConfig {
6487 lines: 6,
6488 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6489 capo: 0,
6490 }),
6491 }),
6492 &mut score,
6493 )
6494 .expect("tablature configuration should apply");
6495 stack
6496 .execute(
6497 Command::SetStaffPresentation(SetStaffPresentationCmd {
6498 part_index: 0,
6499 staff_index: 0,
6500 presentation: tab.clone(),
6501 }),
6502 &mut score,
6503 )
6504 .expect("staff presentation should apply");
6505 assert_eq!(stack.undo_key().as_deref(), Some("SetStaffPresentation"));
6506 assert_eq!(score.parts[0].staves[0].presentation, tab);
6507 stack
6508 .undo(&mut score)
6509 .expect("presentation undo should apply");
6510 assert_eq!(
6511 score.parts[0].staves[0].presentation.kind,
6512 StaffKind::Tablature
6513 );
6514 stack
6515 .redo(&mut score)
6516 .expect("presentation redo should apply");
6517 assert_eq!(
6518 score.parts[0].staves[0].presentation.kind,
6519 StaffKind::Tablature
6520 );
6521 }
6522
6523 #[test]
6524 fn set_instrument_definition_is_undoable_and_rejects_invalid_ranges() {
6525 let mut stack = CommandStack::new(50);
6526 let mut score = default_engine_score();
6527 let mut invalid = InstrumentDefinition::new("violin", "Violin");
6528 invalid.written_range = Some(InstrumentRange {
6529 lowest: 100,
6530 highest: 55,
6531 });
6532 let before = score.clone();
6533 assert!(
6534 stack
6535 .execute(
6536 Command::SetInstrumentDefinition(SetInstrumentDefinitionCmd {
6537 part_index: 0,
6538 definition: Some(invalid),
6539 }),
6540 &mut score,
6541 )
6542 .is_err()
6543 );
6544 assert_eq!(score.parts[0].instrument, before.parts[0].instrument);
6545
6546 let mut definition = InstrumentDefinition::new("violin", "Violin");
6547 definition.short_name = "Vln.".to_string();
6548 definition.family = Some("strings".to_string());
6549 definition.written_range = Some(InstrumentRange {
6550 lowest: 55,
6551 highest: 103,
6552 });
6553 definition.sounding_range = definition.written_range;
6554 definition.default_clefs = vec![Clef::Treble];
6555 definition.midi_program = 40;
6556 let command = Command::SetInstrumentDefinition(SetInstrumentDefinitionCmd {
6557 part_index: 0,
6558 definition: Some(definition.clone()),
6559 });
6560 let json = serde_json::to_string(&command).expect("command should serialize");
6561 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6562 assert_eq!(command_key(&decoded), "SetInstrumentDefinition");
6563 stack
6564 .execute(decoded, &mut score)
6565 .expect("definition applies");
6566 assert_eq!(score.parts[0].instrument, Some(definition));
6567 stack.undo(&mut score).expect("definition undo applies");
6568 assert!(score.parts[0].instrument.is_none());
6569 stack.redo(&mut score).expect("definition redo applies");
6570 assert_eq!(
6571 score.parts[0]
6572 .instrument
6573 .as_ref()
6574 .map(|value| value.id.as_str()),
6575 Some("violin")
6576 );
6577 }
6578
6579 #[test]
6580 fn set_percussion_kit_is_undoable_and_rejects_invalid_entries() {
6581 let mut stack = CommandStack::new(50);
6582 let mut score = default_engine_score();
6583 let invalid = Command::SetPercussionKit(SetPercussionKitCmd {
6584 part_index: 0,
6585 instruments: vec![PercussionInstrument {
6586 id: "snare".to_string(),
6587 name: None,
6588 midi_unpitched: Some(38),
6589 staff_position: Some(33),
6590 notehead: None,
6591 preferred_voice: None,
6592 techniques: Vec::new(),
6593 }],
6594 });
6595 assert!(stack.execute(invalid, &mut score).is_err());
6596 assert!(score.parts[0].percussion_instruments.is_empty());
6597
6598 let kit = vec![PercussionInstrument {
6599 id: "snare".to_string(),
6600 name: Some("Acoustic Snare".to_string()),
6601 midi_unpitched: Some(38),
6602 staff_position: Some(0),
6603 notehead: Some(NoteHead::Cross),
6604 preferred_voice: Some(1),
6605 techniques: vec!["rim-shot".to_string()],
6606 }];
6607 let command = Command::SetPercussionKit(SetPercussionKitCmd {
6608 part_index: 0,
6609 instruments: kit.clone(),
6610 });
6611 let json = serde_json::to_string(&command).expect("command serializes");
6612 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6613 assert_eq!(command_key(&decoded), "SetPercussionKit");
6614 stack.execute(decoded, &mut score).expect("kit applies");
6615 assert_eq!(score.parts[0].percussion_instruments, kit);
6616 stack.undo(&mut score).expect("kit undo applies");
6617 assert!(score.parts[0].percussion_instruments.is_empty());
6618 stack.redo(&mut score).expect("kit redo applies");
6619 assert_eq!(score.parts[0].percussion_instruments, kit);
6620 }
6621
6622 #[test]
6623 fn set_measure_instrument_change_is_undoable_and_json_compatible() {
6624 let mut stack = CommandStack::new(50);
6625 let mut score = default_engine_score();
6626 let mut definition = InstrumentDefinition::new("clarinet-bb", "B-flat Clarinet");
6627 definition.transpose_semitones = -2;
6628 definition.midi_program = 71;
6629 let command = Command::SetMeasureInstrumentChange(SetMeasureInstrumentChangeCmd {
6630 part_index: 0,
6631 staff_index: 0,
6632 measure_index: 2,
6633 definition: Some(definition.clone()),
6634 });
6635 let json = serde_json::to_string(&command).expect("command should serialize");
6636 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6637 assert_eq!(command_key(&decoded), "SetMeasureInstrumentChange");
6638 stack.execute(decoded, &mut score).expect("change applies");
6639 assert_eq!(
6640 score.parts[0].staves[0].measures[2].instrument_change,
6641 Some(definition)
6642 );
6643 stack.undo(&mut score).expect("undo applies");
6644 assert!(
6645 score.parts[0].staves[0].measures[2]
6646 .instrument_change
6647 .is_none()
6648 );
6649 }
6650
6651 #[test]
6652 fn set_measure_tablature_change_is_undoable_and_uses_base_line_count() {
6653 let mut stack = CommandStack::new(50);
6654 let mut score = default_engine_score();
6655 let base = crate::TablatureConfig {
6656 lines: 6,
6657 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6658 capo: 0,
6659 };
6660 score.parts[0].staves[0].tablature = Some(base.clone());
6661 let mut changed = base;
6662 changed.tuning_midi[0] = 38;
6663 changed.capo = 2;
6664 let command = Command::SetMeasureTablatureChange(SetMeasureTablatureChangeCmd {
6665 part_index: 0,
6666 staff_index: 0,
6667 measure_index: 2,
6668 config: Some(changed.clone()),
6669 });
6670 let json = serde_json::to_string(&command).expect("command serializes");
6671 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6672 assert_eq!(command_key(&decoded), "SetMeasureTablatureChange");
6673 stack.execute(decoded, &mut score).expect("change applies");
6674 assert_eq!(
6675 score.parts[0].staves[0].tablature_at(1),
6676 score.parts[0].staves[0].tablature
6677 );
6678 assert_eq!(score.parts[0].staves[0].tablature_at(2), Some(changed));
6679 stack.undo(&mut score).expect("change undo applies");
6680 assert!(
6681 score.parts[0].staves[0].measures[2]
6682 .tablature_change
6683 .is_none()
6684 );
6685
6686 let invalid = Command::SetMeasureTablatureChange(SetMeasureTablatureChangeCmd {
6687 part_index: 0,
6688 staff_index: 0,
6689 measure_index: 1,
6690 config: Some(crate::TablatureConfig {
6691 lines: 7,
6692 tuning_midi: vec![40, 45, 50, 55, 59, 64, 69],
6693 capo: 0,
6694 }),
6695 });
6696 assert!(stack.execute(invalid, &mut score).is_err());
6697 }
6698
6699 #[test]
6700 fn batch_execute_empty_is_noop() {
6701 let mut stack = CommandStack::new(50);
6702 let mut score = default_engine_score();
6703 stack.batch_execute(vec![], &mut score).unwrap();
6704 assert!(!stack.can_undo());
6705 }
6706
6707 #[test]
6710 fn batch_label_used_as_command_key() {
6711 let cmd = Command::Batch(BatchCmd {
6712 commands: vec![],
6713 label: Some("ApplyAI".to_string()),
6714 });
6715 assert_eq!(command_key(&cmd), "ApplyAI");
6716 }
6717
6718 #[test]
6719 fn batch_no_label_key_is_batch() {
6720 let cmd = Command::Batch(BatchCmd {
6721 commands: vec![],
6722 label: None,
6723 });
6724 assert_eq!(command_key(&cmd), "Batch");
6725 }
6726
6727 #[test]
6728 fn batch_label_survives_json_roundtrip() {
6729 let cmd = Command::Batch(BatchCmd {
6730 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 120 })],
6731 label: Some("PasteSelection".to_string()),
6732 });
6733 let json = serde_json::to_string(&cmd).unwrap();
6734 let cmd2: Command = serde_json::from_str(&json).unwrap();
6735 assert_eq!(command_key(&cmd2), "PasteSelection");
6736 }
6737
6738 #[test]
6739 fn batch_label_in_undo_key() {
6740 let mut stack = CommandStack::new(50);
6741 let mut score = default_engine_score();
6742 let cmd = Command::Batch(BatchCmd {
6743 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 140 })],
6744 label: Some("ApplyAI".to_string()),
6745 });
6746 stack.execute(cmd, &mut score).unwrap();
6747 assert_eq!(stack.undo_key(), Some("ApplyAI".to_string()));
6748 }
6749
6750 #[test]
6751 fn undo_returns_change_hint() {
6752 use crate::model::change_hint::ChangeScope;
6753 let mut stack = CommandStack::new(50);
6754 let mut score = default_engine_score();
6755 stack
6756 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
6757 .unwrap();
6758 let hint = stack.undo(&mut score).unwrap();
6759 assert_eq!(hint.scope, ChangeScope::Global);
6760 assert!(hint.playback_dirty);
6761 }
6762
6763 #[test]
6764 fn redo_returns_change_hint() {
6765 use crate::model::change_hint::ChangeScope;
6766 let mut stack = CommandStack::new(50);
6767 let mut score = default_engine_score();
6768 stack
6769 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
6770 .unwrap();
6771 stack.undo(&mut score).unwrap();
6772 let hint = stack.redo(&mut score).unwrap();
6773 assert_eq!(hint.scope, ChangeScope::Global);
6774 assert!(hint.playback_dirty);
6775 }
6776
6777 #[test]
6780 fn toggle_slur_sets_start_and_end() {
6781 let mut score = default_engine_score();
6782 let cmd = Command::AddNote(AddNoteCmd {
6783 part_index: 0,
6784 staff_index: 0,
6785 measure_index: 0,
6786 voice: 0,
6787 position: 0,
6788 pitch: Some(Pitch::new(Step::C, 4)),
6789 duration: Duration::Quarter,
6790 dot_count: 0,
6791 is_rest: false,
6792 tuplet: None,
6793 });
6794 apply_command(&cmd, &mut score).unwrap();
6795 apply_command(
6796 &Command::AddNote(AddNoteCmd {
6797 part_index: 0,
6798 staff_index: 0,
6799 measure_index: 0,
6800 voice: 0,
6801 position: 1,
6802 pitch: Some(Pitch::new(Step::D, 4)),
6803 duration: Duration::Quarter,
6804 dot_count: 0,
6805 is_rest: false,
6806 tuplet: None,
6807 }),
6808 &mut score,
6809 )
6810 .unwrap();
6811 let start = NoteAddr {
6812 part: 0,
6813 staff: 0,
6814 measure: 0,
6815 voice: 0,
6816 note: 0,
6817 };
6818 let end = NoteAddr {
6819 part: 0,
6820 staff: 0,
6821 measure: 0,
6822 voice: 0,
6823 note: 1,
6824 };
6825 apply_command(
6826 &Command::ToggleSlur(ToggleSlurCmd {
6827 start: start.clone(),
6828 end: end.clone(),
6829 }),
6830 &mut score,
6831 )
6832 .unwrap();
6833 assert!(score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
6834 assert!(score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
6835 apply_command(
6837 &Command::ToggleSlur(ToggleSlurCmd { start, end }),
6838 &mut score,
6839 )
6840 .unwrap();
6841 assert!(!score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
6842 assert!(!score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
6843 }
6844
6845 #[test]
6848 fn add_staff_appends_staff_with_correct_measure_count() {
6849 let mut score = default_engine_score();
6850 let before = score.parts[0].staves.len();
6851 let measure_count = score.parts[0].staves[0].measures.len();
6852 apply_command(
6853 &Command::AddStaff(AddStaffCmd {
6854 part_index: 0,
6855 clef: Clef::Bass,
6856 }),
6857 &mut score,
6858 )
6859 .unwrap();
6860 assert_eq!(score.parts[0].staves.len(), before + 1);
6861 let new_staff = score.parts[0].staves.last().unwrap();
6862 assert_eq!(new_staff.measures.len(), measure_count);
6863 }
6864
6865 #[test]
6866 fn add_staff_out_of_range_returns_err() {
6867 let mut score = default_engine_score();
6868 let result = apply_command(
6869 &Command::AddStaff(AddStaffCmd {
6870 part_index: 99,
6871 clef: Clef::Treble,
6872 }),
6873 &mut score,
6874 );
6875 assert!(result.is_err());
6876 }
6877
6878 #[test]
6879 fn delete_staff_removes_extra_staff() {
6880 let mut score = default_engine_score();
6881 apply_command(
6882 &Command::AddStaff(AddStaffCmd {
6883 part_index: 0,
6884 clef: Clef::Bass,
6885 }),
6886 &mut score,
6887 )
6888 .unwrap();
6889 assert_eq!(score.parts[0].staves.len(), 2);
6890 apply_command(
6891 &Command::DeleteStaff(DeleteStaffCmd {
6892 part_index: 0,
6893 staff_index: 1,
6894 }),
6895 &mut score,
6896 )
6897 .unwrap();
6898 assert_eq!(score.parts[0].staves.len(), 1);
6899 }
6900
6901 #[test]
6902 fn delete_last_staff_returns_err() {
6903 let mut score = default_engine_score();
6904 assert_eq!(score.parts[0].staves.len(), 1);
6905 let result = apply_command(
6906 &Command::DeleteStaff(DeleteStaffCmd {
6907 part_index: 0,
6908 staff_index: 0,
6909 }),
6910 &mut score,
6911 );
6912 assert!(result.is_err());
6913 }
6914
6915 #[test]
6918 fn set_tuplet_assigns_and_clears() {
6919 use crate::model::notation::TupletInfo;
6920 let mut score = default_engine_score();
6921 apply_command(
6922 &Command::AddNote(AddNoteCmd {
6923 part_index: 0,
6924 staff_index: 0,
6925 measure_index: 0,
6926 voice: 0,
6927 position: 0,
6928 pitch: Some(Pitch::new(Step::C, 4)),
6929 duration: Duration::Quarter,
6930 dot_count: 0,
6931 is_rest: false,
6932 tuplet: None,
6933 }),
6934 &mut score,
6935 )
6936 .unwrap();
6937 let ti = TupletInfo {
6938 actual_notes: 3,
6939 normal_notes: 2,
6940 };
6941 apply_command(
6942 &Command::SetTuplet(SetTupletCmd {
6943 part_index: 0,
6944 staff_index: 0,
6945 measure_index: 0,
6946 voice_index: 0,
6947 note_index: 0,
6948 tuplet: Some(ti.clone()),
6949 }),
6950 &mut score,
6951 )
6952 .unwrap();
6953 assert_eq!(
6954 score.parts[0].staves[0].measures[0].voices[0][0].tuplet,
6955 Some(ti)
6956 );
6957 apply_command(
6958 &Command::SetTuplet(SetTupletCmd {
6959 part_index: 0,
6960 staff_index: 0,
6961 measure_index: 0,
6962 voice_index: 0,
6963 note_index: 0,
6964 tuplet: None,
6965 }),
6966 &mut score,
6967 )
6968 .unwrap();
6969 assert!(
6970 score.parts[0].staves[0].measures[0].voices[0][0]
6971 .tuplet
6972 .is_none()
6973 );
6974 }
6975
6976 #[test]
6979 fn respell_score_cmd_changes_all_pitches() {
6980 use crate::model::pitch::Step;
6981 let mut score = default_engine_score();
6982 apply_command(
6983 &Command::AddNote(AddNoteCmd {
6984 part_index: 0,
6985 staff_index: 0,
6986 measure_index: 0,
6987 voice: 0,
6988 position: 0,
6989 pitch: Some(Pitch::with_alter(Step::C, 4, 1)), duration: Duration::Quarter,
6991 dot_count: 0,
6992 is_rest: false,
6993 tuplet: None,
6994 }),
6995 &mut score,
6996 )
6997 .unwrap();
6998 apply_command(
6999 &Command::RespellScore(RespellScoreCmd { prefer_flat: true }),
7000 &mut score,
7001 )
7002 .unwrap();
7003 let pitch = &score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
7004 assert_eq!(pitch.step, Step::D);
7005 assert_eq!(pitch.alter, -1); }
7007
7008 fn respell_fixture(measures: u32) -> Score {
7009 use crate::model::pitch::Step;
7010 let mut score = Score::new("Respell", 120, 4, 4, 0, measures);
7011 for measure in &mut score.parts[0].staves[0].measures {
7012 measure.voices[0] = vec![Note::new(Pitch::with_alter(Step::C, 4, 1), Duration::Whole)];
7013 }
7014 score
7015 }
7016
7017 fn spelled(score: &Score, measure: usize) -> (crate::model::pitch::Step, i8) {
7018 let pitch = &score.parts[0].staves[0].measures[measure].voices[0][0].pitches[0];
7019 (pitch.step.clone(), pitch.alter)
7020 }
7021
7022 #[test]
7023 fn respell_staff_region_changes_only_the_selection_and_is_undoable() {
7024 use crate::model::pitch::Step;
7025 let mut score = respell_fixture(4);
7026 let mut stack = CommandStack::new(50);
7027 let command = Command::RespellStaffRegion(RespellStaffRegionCmd {
7028 part_index: 0,
7029 staff_index: 0,
7030 start_measure: 1,
7031 end_measure: 3,
7032 policy: RespellPolicy::Flat,
7033 });
7034 let json = serde_json::to_string(&command).expect("command serializes");
7035 assert!(json.contains("\"policy\":\"flat\""));
7036 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7037 assert_eq!(command_key(&decoded), "RespellStaffRegion");
7038 assert_eq!(command_label(&decoded), "Respell Pitches (flat)");
7039 stack.execute(decoded, &mut score).expect("region respells");
7040 assert_eq!(spelled(&score, 0), (Step::C, 1));
7041 assert_eq!(spelled(&score, 1), (Step::D, -1));
7042 assert_eq!(spelled(&score, 2), (Step::D, -1));
7043 assert_eq!(spelled(&score, 3), (Step::C, 1));
7044 stack.undo(&mut score).expect("region respell undoes");
7045 assert!((0..4).all(|measure| spelled(&score, measure) == (Step::C, 1)));
7046 stack.redo(&mut score).expect("region respell redoes");
7047 assert_eq!(spelled(&score, 2), (Step::D, -1));
7048 }
7049
7050 #[test]
7051 fn respell_staff_region_keeps_ties_across_the_boundary_in_one_spelling() {
7052 use crate::model::pitch::Step;
7053 let mut score = respell_fixture(4);
7054 {
7055 let measures = &mut score.parts[0].staves[0].measures;
7056 measures[0].voices[0][0].tie_start = true;
7057 measures[1].voices[0][0].tie_end = true;
7058 measures[1].voices[0][0].tie_start = true;
7059 measures[2].voices[0][0].tie_end = true;
7060 }
7061 let changed = respell_staff_region(&mut score, 0, 0, 1, 2, RespellPolicy::Flat)
7062 .expect("tied region respells");
7063 assert_eq!(changed, (0, 2));
7064 assert_eq!(spelled(&score, 0), (Step::D, -1));
7065 assert_eq!(spelled(&score, 1), (Step::D, -1));
7066 assert_eq!(spelled(&score, 2), (Step::D, -1));
7067 assert_eq!(spelled(&score, 3), (Step::C, 1));
7068 }
7069
7070 #[test]
7071 fn respell_staff_region_follows_local_keys_and_skips_unpitched_notes() {
7072 use crate::model::notation::KeySignature;
7073 use crate::model::pitch::Step;
7074 let mut score = respell_fixture(4);
7075 for measure in &mut score.parts[0].staves[0].measures {
7076 measure.voices[0][0].pitches[0] = Pitch::with_alter(Step::D, 4, -1);
7077 }
7078 score.parts[0].staves[0].measures[2].key_sig = Some(KeySignature {
7079 fifths: -3,
7080 mode: "major".into(),
7081 });
7082 score.parts[0].staves[0].measures[1].voices[0][0].is_unpitched = true;
7083 respell_staff_region(&mut score, 0, 0, 0, 4, RespellPolicy::Key).expect("key respell");
7084 assert_eq!(spelled(&score, 0), (Step::C, 1));
7085 assert_eq!(
7086 spelled(&score, 1),
7087 (Step::D, -1),
7088 "unpitched staff position is kept"
7089 );
7090 assert_eq!(spelled(&score, 2), (Step::D, -1));
7091 assert_eq!(spelled(&score, 3), (Step::D, -1));
7092
7093 assert!(matches!(
7094 respell_staff_region(&mut score, 0, 0, 2, 2, RespellPolicy::Sharp),
7095 Err(Error::InvalidCommand(_))
7096 ));
7097 assert!(matches!(
7098 respell_staff_region(&mut score, 0, 5, 0, 1, RespellPolicy::Sharp),
7099 Err(Error::StaffNotFound(5))
7100 ));
7101 assert!(matches!(
7102 respell_staff_region(&mut score, 0, 0, 0, 5, RespellPolicy::Sharp),
7103 Err(Error::InvalidCommand(_))
7104 ));
7105 }
7106
7107 #[test]
7108 fn pickup_measure_length_limits_editing_and_survives_time_signature_changes() {
7109 use crate::model::score::MeasureLength;
7110 let mut score = Score::new("Pickup", 120, 4, 4, 0, 2);
7111 score.parts[0].staves[0].measures[0].actual_length = Some(MeasureLength {
7112 numerator: 1,
7113 denominator: 4,
7114 });
7115 score.parts[0].staves[0].measures[0].voices[0] = vec![Note::rest(Duration::Quarter)];
7116 assert!(validate(&score).is_valid());
7117 assert_eq!(
7118 crate::measure_beats_remaining(&score, 0, 0, 0, 0).expect("capacity"),
7119 0.0
7120 );
7121 let mut stack = CommandStack::new(50);
7122 stack
7123 .execute(
7124 Command::AddNote(AddNoteCmd {
7125 part_index: 0,
7126 staff_index: 0,
7127 measure_index: 0,
7128 voice: 0,
7129 position: 0,
7130 pitch: Some(Pitch::new(Step::G, 4)),
7131 duration: Duration::Quarter,
7132 dot_count: 0,
7133 is_rest: false,
7134 tuplet: None,
7135 }),
7136 &mut score,
7137 )
7138 .expect("note fits the pickup after trimming");
7139 let pickup = &score.parts[0].staves[0].measures[0].voices[0];
7140 assert_eq!(pickup.len(), 1);
7141 assert!(!pickup[0].is_rest);
7142
7143 stack
7144 .execute(
7145 Command::SetTimeSignature(SetTimeSignatureCmd {
7146 numerator: 3,
7147 denominator: 4,
7148 }),
7149 &mut score,
7150 )
7151 .expect("time signature changes");
7152 let beats: f64 = score.parts[0].staves[0].measures[0].voices[0]
7153 .iter()
7154 .map(|note| note.beats())
7155 .sum();
7156 assert!((beats - 1.0).abs() < 1e-9, "the pickup keeps one beat");
7157
7158 score.parts[0].staves[0].measures[1].actual_length = Some(MeasureLength {
7159 numerator: 0,
7160 denominator: 4,
7161 });
7162 assert!(validate(&score).errors.iter().any(|error| matches!(
7163 error,
7164 crate::ValidationError::InvalidMeasureLength { measure: 1, .. }
7165 )));
7166 }
7167
7168 #[test]
7169 fn set_lyric_addresses_verses_without_touching_verse_one() {
7170 use crate::model::notation::VerseLyric;
7171 let mut score = Score::new("Verses", 120, 4, 4, 0, 1);
7172 score.parts[0].staves[0].measures[0].voices[0] =
7173 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
7174 let lyric = |text: &str| Lyric {
7175 text: text.into(),
7176 syllabic: "single".into(),
7177 extend: false,
7178 };
7179 let set = |verse: Option<u8>, text: Option<&str>| {
7180 Command::SetLyric(SetLyricCmd {
7181 part_index: 0,
7182 staff_index: 0,
7183 measure_index: 0,
7184 voice: 0,
7185 note_index: 0,
7186 lyric: text.map(lyric),
7187 verse,
7188 })
7189 };
7190 let mut stack = CommandStack::new(50);
7191 stack
7192 .execute(set(None, Some("one")), &mut score)
7193 .expect("verse 1");
7194 stack
7195 .execute(set(Some(3), Some("three")), &mut score)
7196 .expect("verse 3");
7197 stack
7198 .execute(set(Some(2), Some("two")), &mut score)
7199 .expect("verse 2");
7200 stack
7201 .execute(set(Some(2), Some("TWO")), &mut score)
7202 .expect("replace verse 2");
7203 let note = |score: &Score| score.parts[0].staves[0].measures[0].voices[0][0].clone();
7204 assert_eq!(note(&score).lyric, Some(lyric("one")));
7205 assert_eq!(
7206 note(&score).additional_lyrics,
7207 vec![
7208 VerseLyric {
7209 verse: 2,
7210 lyric: lyric("TWO")
7211 },
7212 VerseLyric {
7213 verse: 3,
7214 lyric: lyric("three")
7215 }
7216 ]
7217 );
7218 stack
7219 .execute(set(Some(3), None), &mut score)
7220 .expect("clear verse 3");
7221 assert_eq!(note(&score).additional_lyrics.len(), 1);
7222 stack.undo(&mut score).expect("clear undoes");
7223 assert_eq!(note(&score).additional_lyrics.len(), 2);
7224
7225 let current = serde_json::to_string(&set(None, None)).expect("SetLyric serializes");
7227 assert!(current.contains("\"verse\":null"));
7228 let legacy: Command = serde_json::from_str(¤t.replace(",\"verse\":null", ""))
7229 .expect("legacy SetLyric JSON");
7230 stack
7231 .execute(legacy, &mut score)
7232 .expect("legacy clears verse 1");
7233 assert_eq!(note(&score).lyric, None);
7234 assert_eq!(note(&score).additional_lyrics.len(), 2);
7235
7236 assert!(matches!(
7237 apply_command(&set(Some(0), Some("zero")), &mut score),
7238 Err(Error::InvalidCommand(_))
7239 ));
7240 assert!(matches!(
7241 apply_command(&set(Some(33), Some("too far")), &mut score),
7242 Err(Error::InvalidCommand(_))
7243 ));
7244 score.parts[0].staves[0].measures[0].voices[0][0]
7245 .additional_lyrics
7246 .push(VerseLyric {
7247 verse: 2,
7248 lyric: lyric("duplicate"),
7249 });
7250 assert!(validate(&score).errors.iter().any(|error| matches!(
7251 error,
7252 crate::ValidationError::InvalidLyricVerse { verse: 2, .. }
7253 )));
7254 }
7255
7256 #[test]
7257 fn cycle_enharmonic_spelling_targets_one_chord_member_and_undoes() {
7258 let mut score = Score::new("Enharmonic", 120, 4, 4, 0, 1);
7259 let mut chord = Note::new(Pitch::with_alter(Step::C, 4, 1), Duration::Whole);
7260 chord.pitches.push(Pitch::with_alter(Step::G, 4, 1));
7261 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
7262 let mut stack = CommandStack::new(50);
7263 let command = Command::CycleEnharmonicSpelling(CycleEnharmonicSpellingCmd {
7264 part_index: 0,
7265 staff_index: 0,
7266 measure_index: 0,
7267 voice: 0,
7268 note_index: 0,
7269 pitch_index: Some(1),
7270 });
7271 let json = serde_json::to_string(&command).expect("command serializes");
7272 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7273 assert_eq!(command_key(&decoded), "CycleEnharmonicSpelling");
7274 stack.execute(decoded, &mut score).expect("spelling cycles");
7275 let pitches = |score: &Score| {
7276 score.parts[0].staves[0].measures[0].voices[0][0]
7277 .pitches
7278 .clone()
7279 };
7280 assert_eq!(
7281 pitches(&score),
7282 vec![
7283 Pitch::with_alter(Step::C, 4, 1),
7284 Pitch::with_alter(Step::A, 4, -1)
7285 ]
7286 );
7287 stack.undo(&mut score).expect("spelling undoes");
7288 assert_eq!(pitches(&score)[1], Pitch::with_alter(Step::G, 4, 1));
7289
7290 let mut rest_score = Score::new("Rest", 120, 4, 4, 0, 1);
7291 rest_score.parts[0].staves[0].measures[0].voices[0] = vec![Note::rest(Duration::Whole)];
7292 assert!(matches!(
7293 apply_command(&command, &mut rest_score),
7294 Err(Error::InvalidCommand(_))
7295 ));
7296 }
7297
7298 #[test]
7299 fn resequence_rehearsal_marks_continues_the_first_sequence() {
7300 let mut score = Score::new("Marks", 120, 4, 4, 0, 30);
7301 let mut stack = CommandStack::new(50);
7302 for (index, text) in [(0, "A"), (4, "C"), (9, "C"), (12, "Q")] {
7303 for_each_measure_at(&mut score, index, |measure| {
7304 measure.rehearsal = Some(text.into())
7305 });
7306 }
7307 let command = Command::ResequenceRehearsalMarks(ResequenceRehearsalMarksCmd::default());
7308 let json = serde_json::to_string(&command).expect("command serializes");
7309 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7310 assert_eq!(command_key(&decoded), "ResequenceRehearsalMarks");
7311 stack
7312 .execute(decoded, &mut score)
7313 .expect("marks resequence");
7314 let marks = |score: &Score| -> Vec<Option<String>> {
7315 [0, 4, 9, 12]
7316 .iter()
7317 .map(|&index| score.parts[0].staves[0].measures[index].rehearsal.clone())
7318 .collect()
7319 };
7320 assert_eq!(
7321 marks(&score),
7322 ["A", "B", "C", "D"].map(|text| Some(text.to_string()))
7323 );
7324 stack.undo(&mut score).expect("resequence undoes");
7325 assert_eq!(marks(&score)[1].as_deref(), Some("C"));
7326
7327 let sequence = RehearsalSequence::detect("Z", 1).expect("letters");
7328 assert_eq!(sequence.nth(1, 1), "AA");
7329 assert_eq!(sequence.nth(2, 1), "AB");
7330 let lower = RehearsalSequence::detect("y", 1).expect("lower letters");
7331 assert_eq!(lower.nth(2, 1), "aa");
7332 assert_eq!(
7333 RehearsalSequence::detect("7", 3)
7334 .expect("numbers")
7335 .nth(2, 9),
7336 "9"
7337 );
7338 assert_eq!(
7339 RehearsalSequence::detect("5", 5)
7340 .expect("measure numbers")
7341 .nth(3, 17),
7342 "17"
7343 );
7344 assert!(RehearsalSequence::detect("Intro", 1).is_none());
7345
7346 for_each_measure_at(&mut score, 0, |measure| {
7347 measure.rehearsal = Some("Intro".into())
7348 });
7349 assert!(matches!(
7350 apply_command(&command, &mut score),
7351 Err(Error::InvalidCommand(_))
7352 ));
7353 }
7354
7355 #[test]
7356 fn system_break_interval_replaces_line_breaks_only() {
7357 let mut score = Score::new("Breaks", 120, 4, 4, 0, 10);
7358 score.parts[0].staves[0].measures[1].system_break = true;
7359 score.parts[0].staves[0].measures[2].page_break = true;
7360 apply_command(
7361 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7362 interval: 4,
7363 start_measure: None,
7364 end_measure: None,
7365 }),
7366 &mut score,
7367 )
7368 .expect("breaks every four measures");
7369 let breaks: Vec<usize> = score.parts[0].staves[0]
7370 .measures
7371 .iter()
7372 .enumerate()
7373 .filter(|(_, measure)| measure.system_break)
7374 .map(|(index, _)| index)
7375 .collect();
7376 assert_eq!(breaks, vec![3, 7]);
7377 assert!(score.parts[0].staves[0].measures[2].page_break);
7378
7379 let mut exact = Score::new("Exact", 120, 4, 4, 0, 8);
7380 apply_command(
7381 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7382 interval: 4,
7383 start_measure: None,
7384 end_measure: None,
7385 }),
7386 &mut exact,
7387 )
7388 .expect("no break after the final measure");
7389 assert!(!exact.parts[0].staves[0].measures[7].system_break);
7390 assert!(exact.parts[0].staves[0].measures[3].system_break);
7391
7392 apply_command(
7393 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7394 interval: 0,
7395 start_measure: Some(0),
7396 end_measure: Some(6),
7397 }),
7398 &mut score,
7399 )
7400 .expect("breaks removed in range");
7401 assert!(!score.parts[0].staves[0].measures[3].system_break);
7402 assert!(score.parts[0].staves[0].measures[7].system_break);
7403 assert!(matches!(
7404 apply_command(
7405 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7406 interval: 2,
7407 start_measure: Some(4),
7408 end_measure: Some(11),
7409 }),
7410 &mut score,
7411 ),
7412 Err(Error::InvalidCommand(_))
7413 ));
7414 }
7415
7416 #[test]
7417 fn remove_trailing_empty_measures_keeps_content_and_final_barline() {
7418 let mut score = Score::new("Trailing", 120, 4, 4, 0, 6);
7419 let mut stack = CommandStack::new(50);
7420 score.parts[0].staves[0].measures[1].voices[0] =
7421 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
7422 score.parts[0].staves[0].measures[5].barline_right = Barline::Final;
7423 let mut fermata_rest = Note::rest(Duration::Whole);
7424 fermata_rest.articulations = vec![Articulation::Fermata];
7425 score.parts[0].staves[0].measures[3].voices[0] = vec![fermata_rest];
7426 stack
7427 .execute(
7428 Command::RemoveTrailingEmptyMeasures(RemoveTrailingEmptyMeasuresCmd {}),
7429 &mut score,
7430 )
7431 .expect("trailing measures removed");
7432 let staff = &score.parts[0].staves[0];
7433 assert_eq!(staff.measures.len(), 4, "a rest with a fermata is content");
7434 assert!(matches!(staff.measures[3].barline_right, Barline::Final));
7435 assert_eq!(staff.measures[3].number, 4);
7436 stack.undo(&mut score).expect("removal undoes");
7437 assert_eq!(score.parts[0].staves[0].measures.len(), 6);
7438
7439 let mut empty = Score::new("Empty", 120, 4, 4, 0, 3);
7440 apply_command(
7441 &Command::RemoveTrailingEmptyMeasures(RemoveTrailingEmptyMeasuresCmd {}),
7442 &mut empty,
7443 )
7444 .expect("empty score keeps one measure");
7445 assert_eq!(empty.parts[0].staves[0].measures.len(), 1);
7446 }
7447
7448 #[test]
7449 fn typed_spanner_commands_are_atomic_and_undoable() {
7450 use crate::model::score::{NotationSpanner, NotationSpannerKind};
7451
7452 let mut score = default_engine_score();
7453 let address = NoteAddr {
7454 part: 0,
7455 staff: 0,
7456 measure: 0,
7457 voice: 0,
7458 note: 0,
7459 };
7460 let spanner = NotationSpanner {
7461 id: "slur-1".to_string(),
7462 kind: NotationSpannerKind::Slur,
7463 start: address.clone(),
7464 end: address,
7465 number: Some(1),
7466 line_type: Some("dashed".to_string()),
7467 text: None,
7468 placement: Some("above".to_string()),
7469 ottava_size: None,
7470 ottava_type: None,
7471 };
7472 let mut stack = CommandStack::new(8);
7473 stack
7474 .execute(Command::AddSpanner(AddSpannerCmd { spanner }), &mut score)
7475 .unwrap();
7476 assert_eq!(score.spanners.len(), 1);
7477 assert_eq!(stack.undo_key().as_deref(), Some("AddSpanner"));
7478
7479 let before_duplicate = score.clone();
7480 let duplicate = score.spanners[0].clone();
7481 assert!(
7482 stack
7483 .execute(
7484 Command::AddSpanner(AddSpannerCmd { spanner: duplicate }),
7485 &mut score,
7486 )
7487 .is_err()
7488 );
7489 assert_eq!(score.spanners, before_duplicate.spanners);
7490
7491 let mut updated = score.spanners[0].clone();
7492 updated.number = Some(2);
7493 stack
7494 .execute(
7495 Command::UpdateSpanner(UpdateSpannerCmd { spanner: updated }),
7496 &mut score,
7497 )
7498 .unwrap();
7499 assert_eq!(score.spanners[0].number, Some(2));
7500 stack.undo(&mut score).unwrap();
7501 assert_eq!(score.spanners[0].number, Some(1));
7502 stack.redo(&mut score).unwrap();
7503 assert_eq!(score.spanners[0].number, Some(2));
7504
7505 stack
7506 .execute(
7507 Command::RemoveSpanner(RemoveSpannerCmd {
7508 id: "slur-1".to_string(),
7509 }),
7510 &mut score,
7511 )
7512 .unwrap();
7513 assert!(score.spanners.is_empty());
7514 stack.undo(&mut score).unwrap();
7515 assert_eq!(score.spanners.len(), 1);
7516 }
7517
7518 #[test]
7519 fn string_quartet_linked_views_roundtrip_and_undo_redo_as_one_contract() {
7520 let mut score = Score::template(ScoreTemplate::StringQuartet);
7521 let mut stack = CommandStack::new(16);
7522 for (part, id, name) in [
7523 (0, "violin-1", "Violin I"),
7524 (1, "violin-2", "Violin II"),
7525 (2, "viola", "Viola"),
7526 (3, "cello", "Cello"),
7527 ] {
7528 stack
7529 .execute(
7530 Command::UpsertScoreView(UpsertScoreViewCmd {
7531 view: ScoreView::linked_part(id, name, part),
7532 }),
7533 &mut score,
7534 )
7535 .expect("linked part view applies");
7536 }
7537 assert_eq!(score.parts.len(), 4);
7538 assert_eq!(score.views.len(), 4);
7539 for (part, view) in score.views.iter().enumerate() {
7540 assert_eq!(view.parts, vec![part]);
7541 assert_eq!(score.resolve_view(&view.id).unwrap().parts.len(), 1);
7542 }
7543
7544 let restored: Score =
7545 serde_json::from_str(&serde_json::to_string(&score).expect("quartet score serializes"))
7546 .expect("quartet score deserializes");
7547 assert_eq!(restored.views, score.views);
7548
7549 for _ in 0..4 {
7550 stack.undo(&mut score).expect("view undo applies");
7551 }
7552 assert!(score.views.is_empty());
7553 for _ in 0..4 {
7554 stack.redo(&mut score).expect("view redo applies");
7555 }
7556 assert_eq!(score.views, restored.views);
7557 }
7558
7559 #[test]
7560 fn section_break_is_atomic_undoable_and_synced_across_staves() {
7561 let mut score = Score::template(ScoreTemplate::Piano);
7562 let original = score.clone();
7563 let mut stack = CommandStack::new(4);
7564 stack
7565 .execute(
7566 Command::SetSectionBreak(SetSectionBreakCmd {
7567 measure_index: 2,
7568 value: true,
7569 }),
7570 &mut score,
7571 )
7572 .expect("section break applies");
7573 assert!(
7574 score
7575 .parts
7576 .iter()
7577 .flat_map(|part| part.staves.iter())
7578 .all(|staff| staff.measures[2].section_break)
7579 );
7580 stack.undo(&mut score).expect("section break undoes");
7581 assert_eq!(
7582 serde_json::to_value(&score).unwrap(),
7583 serde_json::to_value(&original).unwrap()
7584 );
7585
7586 assert!(
7587 stack
7588 .execute(
7589 Command::SetSectionBreak(SetSectionBreakCmd {
7590 measure_index: 99,
7591 value: true,
7592 }),
7593 &mut score,
7594 )
7595 .is_err()
7596 );
7597 assert_eq!(
7598 serde_json::to_value(&score).unwrap(),
7599 serde_json::to_value(&original).unwrap()
7600 );
7601 }
7602}