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