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 position_beats: None,
5313 style: crate::TextStyle::Technique,
5314 text: "dolce".to_string(),
5315 placement: None,
5316 offset_x: None,
5317 offset_y: None,
5318 relative_x: None,
5319 relative_y: None,
5320 }),
5321 };
5322 engine.apply(Command::SetMeasureText(address)).unwrap();
5323 assert_eq!(
5324 engine.score.parts[0].staves[0].measures[0].texts[0],
5325 StyledText {
5326 position_beats: None,
5327 style: crate::TextStyle::Technique,
5328 text: "dolce".to_string(),
5329 placement: None,
5330 offset_x: None,
5331 offset_y: None,
5332 relative_x: None,
5333 relative_y: None,
5334 }
5335 );
5336
5337 engine
5338 .apply(Command::SetMeasureText(SetMeasureTextCmd {
5339 text_index: 0,
5340 text: Some(StyledText {
5341 position_beats: None,
5342 style: crate::TextStyle::RehearsalMark,
5343 text: "A".to_string(),
5344 placement: None,
5345 offset_x: None,
5346 offset_y: None,
5347 relative_x: None,
5348 relative_y: None,
5349 }),
5350 ..SetMeasureTextCmd {
5351 part_index: 0,
5352 staff_index: 0,
5353 measure_index: 0,
5354 text_index: 0,
5355 text: None,
5356 }
5357 }))
5358 .unwrap();
5359 assert_eq!(
5360 engine.score.parts[0].staves[0].measures[0].texts[0].style,
5361 crate::TextStyle::RehearsalMark
5362 );
5363
5364 engine
5365 .apply(Command::SetMeasureText(SetMeasureTextCmd {
5366 text_index: 0,
5367 text: None,
5368 ..SetMeasureTextCmd {
5369 part_index: 0,
5370 staff_index: 0,
5371 measure_index: 0,
5372 text_index: 0,
5373 text: None,
5374 }
5375 }))
5376 .unwrap();
5377 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
5378 engine.undo().unwrap();
5379 assert_eq!(engine.score.parts[0].staves[0].measures[0].texts.len(), 1);
5380 engine.redo().unwrap();
5381 assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
5382 }
5383
5384 #[test]
5385 fn set_measure_text_rejects_invalid_index_atomically_and_round_trips_json() {
5386 let mut score = default_engine_score();
5387 let before = score.clone();
5388 let command = Command::SetMeasureText(SetMeasureTextCmd {
5389 part_index: 0,
5390 staff_index: 0,
5391 measure_index: 0,
5392 text_index: 2,
5393 text: Some(StyledText {
5394 position_beats: None,
5395 style: crate::TextStyle::Expression,
5396 text: "espressivo".to_string(),
5397 placement: None,
5398 offset_x: None,
5399 offset_y: None,
5400 relative_x: None,
5401 relative_y: None,
5402 }),
5403 });
5404 let json = serde_json::to_string(&command).unwrap();
5405 let restored: Command = serde_json::from_str(&json).unwrap();
5406 assert_eq!(
5407 serde_json::to_value(&restored).unwrap(),
5408 serde_json::to_value(&command).unwrap()
5409 );
5410 assert!(apply_command(&restored, &mut score).is_err());
5411 assert_eq!(
5412 serde_json::to_value(&score).unwrap(),
5413 serde_json::to_value(&before).unwrap()
5414 );
5415 }
5416
5417 #[test]
5418 fn set_score_text_supports_append_replace_remove_and_undo_redo() {
5419 let mut engine = crate::ScoreEngine::new();
5420 let text = StyledText {
5421 position_beats: None,
5422 style: crate::TextStyle::Expression,
5423 text: "Title".to_string(),
5424 placement: None,
5425 offset_x: None,
5426 offset_y: None,
5427 relative_x: None,
5428 relative_y: None,
5429 };
5430 engine
5431 .apply(Command::SetScoreText(SetScoreTextCmd {
5432 text_index: 0,
5433 text: Some(text.clone()),
5434 }))
5435 .unwrap();
5436 assert_eq!(engine.score.texts, vec![text.clone()]);
5437
5438 let mut replacement = text.clone();
5439 replacement.text = "Subtitle".to_string();
5440 engine
5441 .apply(Command::SetScoreText(SetScoreTextCmd {
5442 text_index: 0,
5443 text: Some(replacement.clone()),
5444 }))
5445 .unwrap();
5446 assert_eq!(engine.score.texts, vec![replacement]);
5447
5448 engine
5449 .apply(Command::SetScoreText(SetScoreTextCmd {
5450 text_index: 0,
5451 text: None,
5452 }))
5453 .unwrap();
5454 assert!(engine.score.texts.is_empty());
5455 engine.undo().unwrap();
5456 assert_eq!(engine.score.texts.len(), 1);
5457 engine.redo().unwrap();
5458 assert!(engine.score.texts.is_empty());
5459 }
5460
5461 #[test]
5462 fn set_score_style_overrides_is_undoable_and_rejects_invalid_values() {
5463 let mut engine = crate::ScoreEngine::new();
5464 let overrides = vec![ViewStyleOverride {
5465 property: super::super::score::ViewStyleProperty::SystemGap,
5466 value: 2.5,
5467 }];
5468 engine
5469 .apply(Command::SetScoreStyleOverrides(SetScoreStyleOverridesCmd {
5470 overrides: overrides.clone(),
5471 }))
5472 .unwrap();
5473 assert_eq!(engine.score.style_overrides, overrides);
5474 engine.undo().unwrap();
5475 assert!(engine.score.style_overrides.is_empty());
5476 engine.redo().unwrap();
5477 assert_eq!(engine.score.style_overrides, overrides);
5478
5479 let before = engine.score.clone();
5480 assert!(
5481 engine
5482 .apply(Command::SetScoreStyleOverrides(SetScoreStyleOverridesCmd {
5483 overrides: vec![ViewStyleOverride {
5484 property: super::super::score::ViewStyleProperty::TextScale,
5485 value: f32::NAN,
5486 }],
5487 }))
5488 .is_err()
5489 );
5490 assert_eq!(
5491 serde_json::to_value(&engine.score).unwrap(),
5492 serde_json::to_value(&before).unwrap()
5493 );
5494 }
5495
5496 #[test]
5497 fn set_object_style_overrides_is_undoable_and_requires_existing_target() {
5498 let mut engine = crate::ScoreEngine::new();
5499 engine.score.texts.push(StyledText {
5500 position_beats: None,
5501 style: crate::TextStyle::Expression,
5502 text: "Allegro".into(),
5503 placement: None,
5504 offset_x: None,
5505 offset_y: None,
5506 relative_x: None,
5507 relative_y: None,
5508 });
5509 let overrides = vec![ObjectStyleOverride {
5510 target: super::super::score::ObjectStyleTarget::ScoreText { text_index: 0 },
5511 property: super::super::score::ViewStyleProperty::TextScale,
5512 value: 1.2,
5513 provenance: None,
5514 }];
5515 engine
5516 .apply(Command::SetObjectStyleOverrides(
5517 SetObjectStyleOverridesCmd {
5518 overrides: overrides.clone(),
5519 },
5520 ))
5521 .unwrap();
5522 assert_eq!(engine.score.object_style_overrides, overrides);
5523 engine.undo().unwrap();
5524 assert!(engine.score.object_style_overrides.is_empty());
5525
5526 assert!(
5527 engine
5528 .apply(Command::SetObjectStyleOverrides(
5529 SetObjectStyleOverridesCmd {
5530 overrides: vec![ObjectStyleOverride {
5531 target: super::super::score::ObjectStyleTarget::ScoreText {
5532 text_index: 1,
5533 },
5534 property: super::super::score::ViewStyleProperty::TextScale,
5535 value: 1.2,
5536 provenance: None,
5537 }],
5538 }
5539 ))
5540 .is_err()
5541 );
5542 }
5543
5544 #[test]
5545 fn set_harmony_range_is_undoable_and_json_compatible() {
5546 let mut score = default_engine_score();
5547 score.parts[0].staves[0].measures[0].voices[0] =
5548 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
5549 score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
5550 root: "C".to_owned(),
5551 kind: "major".to_owned(),
5552 bass: None,
5553 placement: None,
5554 extender: true,
5555 harmonic_degree: None,
5556 harmony_function: None,
5557 harmony_type: None,
5558 chord_ref: None,
5559 range_end: None,
5560 degrees: Vec::new(),
5561 });
5562 let mut engine = ScoreEngine::new();
5563 engine.replace_score(score);
5564 let command = Command::SetHarmonyRange(SetHarmonyRangeCmd {
5565 part_index: 0,
5566 staff_index: 0,
5567 measure_index: 0,
5568 voice: 0,
5569 note_index: 0,
5570 end: Some(NoteAddr {
5571 part: 0,
5572 staff: 0,
5573 measure: 0,
5574 voice: 0,
5575 note: 0,
5576 }),
5577 });
5578 let json = serde_json::to_string(&command).unwrap();
5579 let restored: Command = serde_json::from_str(&json).unwrap();
5580 engine.apply(restored).unwrap();
5581 assert!(
5582 engine.score.parts[0].staves[0].measures[0].voices[0][0]
5583 .chord_symbol
5584 .as_ref()
5585 .and_then(|chord| chord.range_end.as_ref())
5586 .is_some()
5587 );
5588 engine.undo().unwrap();
5589 assert!(
5590 engine.score.parts[0].staves[0].measures[0].voices[0][0]
5591 .chord_symbol
5592 .as_ref()
5593 .is_some_and(|chord| chord.range_end.is_none())
5594 );
5595 }
5596
5597 #[test]
5598 fn set_harp_pedal_diagrams_is_undoable_and_json_compatible() {
5599 let mut engine = ScoreEngine::new();
5600 let mut diagram = HarpPedalDiagram::default();
5601 diagram.positions[0] = super::super::score::HarpPedalPosition::Flat;
5602 diagram.positions[6] = super::super::score::HarpPedalPosition::Sharp;
5603 let command = Command::SetHarpPedalDiagrams(SetHarpPedalDiagramsCmd {
5604 part_index: 0,
5605 staff_index: 0,
5606 measure_index: 0,
5607 diagrams: vec![diagram.clone()],
5608 });
5609 let restored: Command =
5610 serde_json::from_str(&serde_json::to_string(&command).unwrap()).unwrap();
5611 engine.apply(restored).unwrap();
5612 assert_eq!(
5613 engine.score.parts[0].staves[0].measures[0].harp_pedal_diagrams,
5614 vec![diagram]
5615 );
5616 engine.undo().unwrap();
5617 assert!(
5618 engine.score.parts[0].staves[0].measures[0]
5619 .harp_pedal_diagrams
5620 .is_empty()
5621 );
5622 }
5623
5624 #[test]
5625 fn set_note_placement_is_undoable_and_rejects_non_finite_values() {
5626 let mut score = default_engine_score();
5627 score.parts[0].staves[0].measures[0].voices[0] =
5628 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5629 let mut engine = ScoreEngine::new();
5630 engine.replace_score(score);
5631 let command = Command::SetNotePlacement(SetNotePlacementCmd {
5632 part_index: 0,
5633 staff_index: 0,
5634 measure_index: 0,
5635 voice: 0,
5636 note_index: 0,
5637 offset_x: Some(12.5),
5638 offset_y: Some(-3.0),
5639 relative_x: Some(1.25),
5640 relative_y: Some(-0.5),
5641 });
5642 engine.apply(command).unwrap();
5643 let note = &engine.score.parts[0].staves[0].measures[0].voices[0][0];
5644 assert_eq!(note.offset_x, Some(12.5));
5645 assert_eq!(note.relative_y, Some(-0.5));
5646 engine.undo().unwrap();
5647 assert_eq!(
5648 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
5649 None
5650 );
5651 let invalid = Command::SetNotePlacement(SetNotePlacementCmd {
5652 part_index: 0,
5653 staff_index: 0,
5654 measure_index: 0,
5655 voice: 0,
5656 note_index: 0,
5657 offset_x: Some(f64::NAN),
5658 offset_y: None,
5659 relative_x: None,
5660 relative_y: None,
5661 });
5662 assert!(engine.apply(invalid).is_err());
5663 assert_eq!(
5664 engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
5665 None
5666 );
5667 }
5668
5669 #[test]
5670 fn set_guitar_bend_alter_updates_note_and_can_clear() {
5671 let mut score = default_engine_score();
5672 apply_command(
5673 &Command::AddNote(AddNoteCmd {
5674 part_index: 0,
5675 staff_index: 0,
5676 measure_index: 0,
5677 voice: 0,
5678 position: 0,
5679 pitch: Some(Pitch::new(Step::G, 4)),
5680 duration: Duration::Quarter,
5681 dot_count: 0,
5682 is_rest: false,
5683 tuplet: None,
5684 }),
5685 &mut score,
5686 )
5687 .unwrap();
5688 let location = SetGuitarBendAlterCmd {
5689 part_index: 0,
5690 staff_index: 0,
5691 measure_index: 0,
5692 voice: 0,
5693 note_index: 0,
5694 alter_cents: Some(200),
5695 };
5696 apply_command(&Command::SetGuitarBendAlter(location.clone()), &mut score).unwrap();
5697 assert_eq!(
5698 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
5699 Some(200)
5700 );
5701 let mut cleared = location;
5702 cleared.alter_cents = None;
5703 apply_command(&Command::SetGuitarBendAlter(cleared), &mut score).unwrap();
5704 assert_eq!(
5705 score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
5706 None
5707 );
5708 }
5709
5710 #[test]
5711 fn set_guitar_bend_curve_is_undoable_and_json_compatible() {
5712 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5713 let command = Command::SetGuitarBendCurve(SetGuitarBendCurveCmd {
5714 part_index: 0,
5715 staff_index: 0,
5716 measure_index: 0,
5717 voice: 0,
5718 note_index: 0,
5719 points: vec![
5720 crate::GuitarBendPoint {
5721 position_per_mille: 0,
5722 alter_cents: 0,
5723 },
5724 crate::GuitarBendPoint {
5725 position_per_mille: 500,
5726 alter_cents: 200,
5727 },
5728 crate::GuitarBendPoint {
5729 position_per_mille: 1000,
5730 alter_cents: 0,
5731 },
5732 ],
5733 });
5734 let decoded: Command =
5735 serde_json::from_str(&serde_json::to_string(&command).unwrap()).unwrap();
5736 let mut stack = CommandStack::new(16);
5737 stack.execute(decoded, &mut score).unwrap();
5738 assert_eq!(
5739 score.parts[0].staves[0].measures[0].voices[0][0]
5740 .guitar_bend_curve
5741 .len(),
5742 3
5743 );
5744 stack.undo(&mut score).unwrap();
5745 assert!(
5746 score.parts[0].staves[0].measures[0].voices[0][0]
5747 .guitar_bend_curve
5748 .is_empty()
5749 );
5750 }
5751
5752 #[test]
5753 fn set_duration_updates_note_and_preserves_measure_capacity() {
5754 let mut score = default_engine_score();
5755 apply_command(
5756 &Command::AddNote(AddNoteCmd {
5757 part_index: 0,
5758 staff_index: 0,
5759 measure_index: 0,
5760 voice: 0,
5761 position: 0,
5762 pitch: Some(Pitch::new(Step::C, 4)),
5763 duration: Duration::Quarter,
5764 dot_count: 0,
5765 is_rest: false,
5766 tuplet: None,
5767 }),
5768 &mut score,
5769 )
5770 .unwrap();
5771 apply_command(
5772 &Command::SetDuration(SetDurationCmd {
5773 part_index: 0,
5774 staff_index: 0,
5775 measure_index: 0,
5776 voice: 0,
5777 note_index: 0,
5778 duration: Duration::Half,
5779 dot_count: 1,
5780 }),
5781 &mut score,
5782 )
5783 .unwrap();
5784 let voice = &score.parts[0].staves[0].measures[0].voices[0];
5785 assert_eq!(voice[0].duration, Duration::Half);
5786 assert_eq!(voice[0].dot_count, 1);
5787 assert!((voice.iter().map(|note| note.beats()).sum::<f64>() - 4.0).abs() < 1e-9);
5788 }
5789
5790 #[test]
5791 fn set_tempo_updates_score() {
5792 let mut score = default_engine_score();
5793 let cmd = Command::SetTempo(SetTempoCmd { bpm: 160 });
5794 apply_command(&cmd, &mut score).unwrap();
5795 assert_eq!(score.settings.tempo_bpm, 160);
5796 }
5797
5798 #[test]
5799 fn add_measure_increases_count() {
5800 let mut score = default_engine_score();
5801 let before = score.measure_count();
5802 apply_command(
5803 &Command::AddMeasure(AddMeasureCmd { after_index: 0 }),
5804 &mut score,
5805 )
5806 .unwrap();
5807 assert_eq!(score.measure_count(), before + 1);
5808 }
5809
5810 #[test]
5811 fn delete_measure_decreases_count() {
5812 let mut score = default_engine_score();
5813 let before = score.measure_count();
5814 apply_command(
5815 &Command::DeleteMeasure(DeleteMeasureCmd { measure_index: 0 }),
5816 &mut score,
5817 )
5818 .unwrap();
5819 assert_eq!(score.measure_count(), before - 1);
5820 }
5821
5822 #[test]
5823 fn undo_restores_score() {
5824 let mut stack = CommandStack::new(50);
5825 let mut score = default_engine_score();
5826 let before = score.settings.tempo_bpm;
5827 stack
5828 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
5829 .unwrap();
5830 assert_eq!(score.settings.tempo_bpm, 200);
5831 stack.undo(&mut score).unwrap();
5832 assert_eq!(score.settings.tempo_bpm, before);
5833 }
5834
5835 #[test]
5836 fn redo_reapplies_command() {
5837 let mut stack = CommandStack::new(50);
5838 let mut score = default_engine_score();
5839 stack
5840 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
5841 .unwrap();
5842 stack.undo(&mut score).unwrap();
5843 stack.redo(&mut score).unwrap();
5844 assert_eq!(score.settings.tempo_bpm, 200);
5845 }
5846
5847 #[test]
5848 fn undo_nothing_returns_error() {
5849 let mut stack = CommandStack::new(50);
5850 let mut score = default_engine_score();
5851 assert!(stack.undo(&mut score).is_err());
5852 }
5853
5854 #[test]
5855 fn add_part_appends_part() {
5856 let mut score = default_engine_score();
5857 let before = score.parts.len();
5858 apply_command(
5859 &Command::AddPart(AddPartCmd {
5860 name: "Violin".into(),
5861 short_name: "Vln.".into(),
5862 clefs: vec!["Treble".into()],
5863 midi_channel: 0,
5864 midi_program: 0,
5865 }),
5866 &mut score,
5867 )
5868 .unwrap();
5869 assert_eq!(score.parts.len(), before + 1);
5870 }
5871
5872 #[test]
5873 fn delete_part_removes_part() {
5874 let mut score = default_engine_score();
5875 apply_command(
5876 &Command::AddPart(AddPartCmd {
5877 name: "Violin".into(),
5878 short_name: "V.".into(),
5879 clefs: vec!["Treble".into()],
5880 midi_channel: 0,
5881 midi_program: 0,
5882 }),
5883 &mut score,
5884 )
5885 .unwrap();
5886 let before = score.parts.len();
5887 apply_command(
5888 &Command::DeletePart(DeletePartCmd { part_index: 0 }),
5889 &mut score,
5890 )
5891 .unwrap();
5892 assert_eq!(score.parts.len(), before - 1);
5893 }
5894
5895 #[test]
5896 fn delete_part_out_of_range_returns_err() {
5897 let mut score = default_engine_score();
5898 assert!(
5899 apply_command(
5900 &Command::DeletePart(DeletePartCmd { part_index: 99 }),
5901 &mut score
5902 )
5903 .is_err()
5904 );
5905 }
5906
5907 #[test]
5908 fn delete_part_undo_restores_part() {
5909 let mut stack = CommandStack::new(50);
5910 let mut score = default_engine_score();
5911 apply_command(
5912 &Command::AddPart(AddPartCmd {
5913 name: "Violin".into(),
5914 short_name: "V.".into(),
5915 clefs: vec!["Treble".into()],
5916 midi_channel: 0,
5917 midi_program: 0,
5918 }),
5919 &mut score,
5920 )
5921 .unwrap();
5922 let before = score.parts.len();
5923 stack
5924 .execute(
5925 Command::DeletePart(DeletePartCmd { part_index: 0 }),
5926 &mut score,
5927 )
5928 .unwrap();
5929 assert_eq!(score.parts.len(), before - 1);
5930 stack.undo(&mut score).unwrap();
5931 assert_eq!(score.parts.len(), before);
5932 }
5933
5934 #[test]
5935 fn reorder_parts_remaps_linked_views_and_preserves_undo() {
5936 let mut engine = crate::ScoreEngine::new();
5937 engine
5938 .apply(Command::AddPart(AddPartCmd {
5939 name: "Flute".into(),
5940 short_name: "Fl.".into(),
5941 clefs: vec!["Treble".into()],
5942 midi_channel: 1,
5943 midi_program: 73,
5944 }))
5945 .unwrap();
5946 engine
5947 .score
5948 .views
5949 .push(ScoreView::linked_part("flute", "Flute", 1));
5950 engine.score.part_groups.push(PartGroup {
5951 first_part: 0,
5952 last_part: 1,
5953 symbol: super::super::score::PartGroupSymbol::Bracket,
5954 barlines_connect: false,
5955 });
5956
5957 engine
5958 .apply(Command::ReorderParts(ReorderPartsCmd { order: vec![1, 0] }))
5959 .unwrap();
5960 assert_eq!(engine.score.parts[0].name, "Flute");
5961 assert_eq!(engine.score.views[0].parts, vec![0]);
5962 engine.undo().unwrap();
5963 assert_eq!(engine.score.parts[0].name, "Piano");
5964 assert_eq!(engine.score.views[0].parts, vec![1]);
5965 }
5966
5967 #[test]
5968 fn reorder_parts_rejects_splitting_a_part_group_without_mutation() {
5969 let mut score = Score::template(ScoreTemplate::StringQuartet);
5970 score.part_groups.push(PartGroup {
5971 first_part: 0,
5972 last_part: 1,
5973 symbol: super::super::score::PartGroupSymbol::Bracket,
5974 barlines_connect: false,
5975 });
5976 let before = serde_json::to_value(&score).unwrap();
5977
5978 assert!(
5979 apply_command(
5980 &Command::ReorderParts(ReorderPartsCmd {
5981 order: vec![0, 2, 1, 3],
5982 }),
5983 &mut score,
5984 )
5985 .is_err()
5986 );
5987 assert_eq!(serde_json::to_value(&score).unwrap(), before);
5988 }
5989
5990 #[test]
5991 fn set_metadata_updates_title() {
5992 let mut score = default_engine_score();
5993 apply_command(
5994 &Command::SetMetadata(SetMetadataCmd {
5995 title: Some("New Title".into()),
5996 ..Default::default()
5997 }),
5998 &mut score,
5999 )
6000 .unwrap();
6001 assert_eq!(score.metadata.title, "New Title");
6002 }
6003
6004 #[test]
6005 fn set_metadata_none_fields_skipped() {
6006 let mut score = default_engine_score();
6007 let original_composer = score.metadata.composer.clone();
6008 apply_command(
6009 &Command::SetMetadata(SetMetadataCmd {
6010 title: Some("X".into()),
6011 ..Default::default()
6012 }),
6013 &mut score,
6014 )
6015 .unwrap();
6016 assert_eq!(score.metadata.composer, original_composer);
6017 }
6018
6019 #[test]
6020 fn set_volta_sets_bracket() {
6021 use crate::model::score::VoltaBracket;
6022 let mut score = default_engine_score();
6023 let volta = VoltaBracket {
6024 number: 1,
6025 kind: "begin_end".into(),
6026 numbers: Vec::new(),
6027 };
6028 apply_command(
6029 &Command::SetVolta(SetVoltaCmd {
6030 measure_index: 0,
6031 volta: Some(volta.clone()),
6032 }),
6033 &mut score,
6034 )
6035 .unwrap();
6036 assert!(score.parts[0].staves[0].measures[0].volta.is_some());
6037 }
6038
6039 #[test]
6040 fn set_volta_none_clears_bracket() {
6041 use crate::model::score::VoltaBracket;
6042 let mut score = default_engine_score();
6043 score.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
6044 number: 1,
6045 kind: "begin_end".into(),
6046 numbers: Vec::new(),
6047 });
6048 apply_command(
6049 &Command::SetVolta(SetVoltaCmd {
6050 measure_index: 0,
6051 volta: None,
6052 }),
6053 &mut score,
6054 )
6055 .unwrap();
6056 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
6057 }
6058
6059 #[test]
6060 fn set_volta_undo_restores_old() {
6061 use crate::model::score::VoltaBracket;
6062 let mut stack = CommandStack::new(50);
6063 let mut score = default_engine_score();
6064 stack
6065 .execute(
6066 Command::SetVolta(SetVoltaCmd {
6067 measure_index: 0,
6068 volta: Some(VoltaBracket {
6069 number: 1,
6070 kind: "begin_end".into(),
6071 numbers: Vec::new(),
6072 }),
6073 }),
6074 &mut score,
6075 )
6076 .unwrap();
6077 stack.undo(&mut score).unwrap();
6078 assert!(score.parts[0].staves[0].measures[0].volta.is_none());
6079 }
6080
6081 #[test]
6082 fn set_clef_updates_staff_clef() {
6083 use crate::model::notation::Clef;
6084 let mut score = default_engine_score();
6085 apply_command(
6086 &Command::SetClef(SetClefCmd {
6087 part_index: 0,
6088 staff_index: 0,
6089 clef: Clef::Bass,
6090 }),
6091 &mut score,
6092 )
6093 .unwrap();
6094 assert_eq!(score.parts[0].staves[0].clef, Clef::Bass);
6095 }
6096
6097 #[test]
6098 fn set_clef_out_of_range_returns_err() {
6099 use crate::model::notation::Clef;
6100 let mut score = default_engine_score();
6101 assert!(
6102 apply_command(
6103 &Command::SetClef(SetClefCmd {
6104 part_index: 99,
6105 staff_index: 0,
6106 clef: Clef::Bass,
6107 }),
6108 &mut score
6109 )
6110 .is_err()
6111 );
6112 }
6113
6114 #[test]
6115 fn set_part_name_updates_name() {
6116 let mut score = default_engine_score();
6117 apply_command(
6118 &Command::SetPartName(SetPartNameCmd {
6119 part_index: 0,
6120 name: "Violin".into(),
6121 short_name: "Vln.".into(),
6122 }),
6123 &mut score,
6124 )
6125 .unwrap();
6126 assert_eq!(score.parts[0].name, "Violin");
6127 assert_eq!(score.parts[0].short_name, "Vln.");
6128 }
6129
6130 #[test]
6131 fn set_part_name_undo_restores_old() {
6132 let mut stack = CommandStack::new(50);
6133 let mut score = default_engine_score();
6134 let original = score.parts[0].name.clone();
6135 stack
6136 .execute(
6137 Command::SetPartName(SetPartNameCmd {
6138 part_index: 0,
6139 name: "Flute".into(),
6140 short_name: "Fl.".into(),
6141 }),
6142 &mut score,
6143 )
6144 .unwrap();
6145 stack.undo(&mut score).unwrap();
6146 assert_eq!(score.parts[0].name, original);
6147 }
6148
6149 #[test]
6150 fn set_metadata_undo_restores_old_title() {
6151 let mut stack = CommandStack::new(50);
6152 let mut score = default_engine_score();
6153 let original = score.metadata.title.clone();
6154 stack
6155 .execute(
6156 Command::SetMetadata(SetMetadataCmd {
6157 title: Some("Changed".into()),
6158 ..Default::default()
6159 }),
6160 &mut score,
6161 )
6162 .unwrap();
6163 assert_ne!(score.metadata.title, original);
6164 stack.undo(&mut score).unwrap();
6165 assert_eq!(score.metadata.title, original);
6166 }
6167
6168 #[test]
6169 fn set_midi_instrument_updates_channel_and_program() {
6170 let mut score = default_engine_score();
6171 apply_command(
6172 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
6173 part_index: 0,
6174 midi_channel: 2,
6175 midi_program: 40,
6176 }),
6177 &mut score,
6178 )
6179 .unwrap();
6180 assert_eq!(score.parts[0].midi_channel, 2);
6181 assert_eq!(score.parts[0].midi_program, 40);
6182 }
6183
6184 #[test]
6185 fn set_midi_instrument_clamps_channel_to_15() {
6186 let mut score = default_engine_score();
6187 apply_command(
6188 &Command::SetMidiInstrument(SetMidiInstrumentCmd {
6189 part_index: 0,
6190 midi_channel: 20,
6191 midi_program: 0,
6192 }),
6193 &mut score,
6194 )
6195 .unwrap();
6196 assert_eq!(score.parts[0].midi_channel, 15);
6197 }
6198
6199 #[test]
6200 fn set_midi_instrument_undo_restores_old() {
6201 let mut stack = CommandStack::new(50);
6202 let mut score = default_engine_score();
6203 score.parts[0].midi_channel = 3;
6204 score.parts[0].midi_program = 10;
6205 stack
6206 .execute(
6207 Command::SetMidiInstrument(SetMidiInstrumentCmd {
6208 part_index: 0,
6209 midi_channel: 9,
6210 midi_program: 114,
6211 }),
6212 &mut score,
6213 )
6214 .unwrap();
6215 stack.undo(&mut score).unwrap();
6216 assert_eq!(score.parts[0].midi_channel, 3);
6217 assert_eq!(score.parts[0].midi_program, 10);
6218 }
6219
6220 #[test]
6221 fn set_transpose_updates_staff() {
6222 let mut score = default_engine_score();
6223 apply_command(
6224 &Command::SetTranspose(SetTransposeCmd {
6225 part_index: 0,
6226 staff_index: 0,
6227 semitones: -2,
6228 }),
6229 &mut score,
6230 )
6231 .unwrap();
6232 assert_eq!(score.parts[0].staves[0].transpose_semitones, -2);
6233 }
6234
6235 #[test]
6236 fn set_transpose_out_of_range_returns_err() {
6237 let mut score = default_engine_score();
6238 assert!(
6239 apply_command(
6240 &Command::SetTranspose(SetTransposeCmd {
6241 part_index: 99,
6242 staff_index: 0,
6243 semitones: -2,
6244 }),
6245 &mut score
6246 )
6247 .is_err()
6248 );
6249 }
6250
6251 #[test]
6252 fn transpose_staff_region_is_undoable_and_json_compatible() {
6253 let mut stack = CommandStack::new(50);
6254 let mut score = Score::new("Region", 120, 4, 4, 0, 2);
6255 for measure in &mut score.parts[0].staves[0].measures {
6256 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
6257 }
6258 let command = Command::TransposeStaffRegion(TransposeStaffRegionCmd {
6259 part_index: 0,
6260 staff_index: 0,
6261 start_measure: 1,
6262 end_measure: 2,
6263 semitones: 2,
6264 target: RegionalTranspositionTarget::Written,
6265 });
6266 let json = serde_json::to_string(&command).expect("command serializes");
6267 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6268 assert_eq!(command_key(&decoded), "TransposeStaffRegion");
6269 stack.execute(decoded, &mut score).expect("region applies");
6270 assert_eq!(
6271 score.parts[0].staves[0].measures[0].voices[0][0].pitches[0].to_midi(),
6272 60
6273 );
6274 assert_eq!(
6275 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0].to_midi(),
6276 62
6277 );
6278 stack.undo(&mut score).expect("region undo applies");
6279 assert_eq!(
6280 score.parts[0].staves[0].measures[1].voices[0][0].pitches[0].to_midi(),
6281 60
6282 );
6283 }
6284
6285 #[test]
6286 fn set_tempo_at_measure_sets_tempo() {
6287 let mut score = default_engine_score();
6288 apply_command(
6289 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
6290 measure_index: 0,
6291 bpm: Some(80),
6292 }),
6293 &mut score,
6294 )
6295 .unwrap();
6296 assert_eq!(score.parts[0].staves[0].measures[0].tempo, Some(80));
6297 }
6298
6299 #[test]
6300 fn set_tempo_at_measure_none_clears_tempo() {
6301 let mut score = default_engine_score();
6302 score.parts[0].staves[0].measures[0].tempo = Some(120);
6303 apply_command(
6304 &Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
6305 measure_index: 0,
6306 bpm: None,
6307 }),
6308 &mut score,
6309 )
6310 .unwrap();
6311 assert!(score.parts[0].staves[0].measures[0].tempo.is_none());
6312 }
6313
6314 #[test]
6315 fn set_tempo_ramp_at_measure_applies() {
6316 let mut score = default_engine_score();
6317 apply_command(
6318 &Command::SetTempoRampAtMeasure(SetTempoRampAtMeasureCmd {
6319 measure_index: 0,
6320 target_bpm: Some(72),
6321 }),
6322 &mut score,
6323 )
6324 .expect("tempo ramp applies");
6325 assert_eq!(score.parts[0].staves[0].measures[0].tempo_ramp_to, Some(72));
6326 }
6327
6328 #[test]
6331 fn batch_execute_two_commands_single_undo() {
6332 let mut stack = CommandStack::new(50);
6333 let mut score = default_engine_score();
6334 let original_bpm = score.settings.tempo_bpm;
6335 stack
6336 .batch_execute(
6337 vec![
6338 Command::SetTempo(SetTempoCmd { bpm: 160 }),
6339 Command::SetTempo(SetTempoCmd { bpm: 180 }),
6340 ],
6341 &mut score,
6342 )
6343 .unwrap();
6344 assert_eq!(score.settings.tempo_bpm, 180);
6345 stack.undo(&mut score).unwrap();
6346 assert_eq!(score.settings.tempo_bpm, original_bpm);
6347 }
6348
6349 #[test]
6350 fn batch_execute_partial_failure_rollback() {
6351 let mut stack = CommandStack::new(50);
6352 let mut score = default_engine_score();
6353 let original_bpm = score.settings.tempo_bpm;
6354 let result = stack.batch_execute(
6355 vec![
6356 Command::SetTempo(SetTempoCmd { bpm: 160 }),
6357 Command::DeleteNote(DeleteNoteCmd {
6358 note_id: "nonexistent".into(),
6359 part_index: 99,
6360 staff_index: 0,
6361 measure_index: 0,
6362 voice: 0,
6363 }),
6364 ],
6365 &mut score,
6366 );
6367 assert!(result.is_err());
6368 assert_eq!(score.settings.tempo_bpm, original_bpm);
6369 }
6370
6371 #[test]
6372 fn execute_rejects_invalid_candidate_without_mutating_or_recording_history() {
6373 let mut stack = CommandStack::new(50);
6374 let mut score = default_engine_score();
6375 score.parts[0].staves[0].tablature = Some(crate::TablatureConfig {
6376 lines: 6,
6377 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6378 capo: 0,
6379 });
6380 stack
6381 .execute(
6382 Command::AddNote(AddNoteCmd {
6383 part_index: 0,
6384 staff_index: 0,
6385 measure_index: 0,
6386 voice: 0,
6387 position: 0,
6388 pitch: Some(Pitch::new(Step::C, 4)),
6389 duration: Duration::Quarter,
6390 dot_count: 0,
6391 is_rest: false,
6392 tuplet: None,
6393 }),
6394 &mut score,
6395 )
6396 .expect("valid pitched note should be accepted");
6397 let original = score.clone();
6398 let result = stack.execute(
6399 Command::SetTabPosition(SetTabPositionCmd {
6400 part_index: 0,
6401 staff_index: 0,
6402 measure_index: 0,
6403 voice: 0,
6404 note_index: 0,
6405 position: Some(crate::TabPosition { string: 7, fret: 0 }),
6406 }),
6407 &mut score,
6408 );
6409 assert!(matches!(result, Err(Error::InvalidScore)));
6410 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
6411 let original_note = &original.parts[0].staves[0].measures[0].voices[0][0];
6412 assert_eq!(note.pitches, original_note.pitches);
6413 assert_eq!(note.duration, original_note.duration);
6414 assert_eq!(note.tab_position, original_note.tab_position);
6415 assert_eq!(note.tab_positions, original_note.tab_positions);
6416 assert!(stack.can_undo());
6417 }
6418
6419 #[test]
6420 fn set_tablature_config_is_undoable_and_json_compatible() {
6421 let mut stack = CommandStack::new(50);
6422 let mut score = default_engine_score();
6423 let config = crate::TablatureConfig {
6424 lines: 6,
6425 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6426 capo: 2,
6427 };
6428 let command = Command::SetTablatureConfig(SetTablatureConfigCmd {
6429 part_index: 0,
6430 staff_index: 0,
6431 config: Some(config.clone()),
6432 });
6433 let json = serde_json::to_string(&command).expect("command should serialize");
6434 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6435 assert_eq!(command_key(&decoded), "SetTablatureConfig");
6436 stack
6437 .execute(decoded, &mut score)
6438 .expect("config should apply");
6439 assert_eq!(score.parts[0].staves[0].tablature, Some(config));
6440 assert_eq!(
6441 score.parts[0].staves[0].presentation.kind,
6442 StaffKind::Tablature
6443 );
6444 stack.undo(&mut score).expect("config undo should apply");
6445 assert!(score.parts[0].staves[0].tablature.is_none());
6446 assert_eq!(
6447 score.parts[0].staves[0].presentation.kind,
6448 StaffKind::Standard
6449 );
6450 stack.redo(&mut score).expect("config redo should apply");
6451 assert_eq!(
6452 score.parts[0].staves[0]
6453 .tablature
6454 .as_ref()
6455 .map(|tab| tab.capo),
6456 Some(2)
6457 );
6458 }
6459
6460 #[test]
6461 fn set_staff_presentation_is_undoable_and_requires_tablature_config() {
6462 let mut stack = CommandStack::new(50);
6463 let mut score = default_engine_score();
6464 let tab = StaffPresentation {
6465 kind: StaffKind::Tablature,
6466 lines: 6,
6467 line_distance: 1.25,
6468 small: true,
6469 cutaway: false,
6470 visible: true,
6471 notehead_scheme: super::super::score::StaffNoteheadScheme::Standard,
6472 tablature_rhythm_display: super::super::score::TablatureRhythmDisplay::FretOnly,
6473 tablature_fret_mark_style: super::super::score::TablatureFretMarkStyle::Arabic,
6474 };
6475 let rejected = Command::SetStaffPresentation(SetStaffPresentationCmd {
6476 part_index: 0,
6477 staff_index: 0,
6478 presentation: tab.clone(),
6479 });
6480 let before = score.clone();
6481 assert!(stack.execute(rejected, &mut score).is_err());
6482 assert_eq!(
6483 score.parts[0].staves[0].presentation,
6484 before.parts[0].staves[0].presentation
6485 );
6486 assert!(!stack.can_undo());
6487
6488 stack
6489 .execute(
6490 Command::SetTablatureConfig(SetTablatureConfigCmd {
6491 part_index: 0,
6492 staff_index: 0,
6493 config: Some(crate::TablatureConfig {
6494 lines: 6,
6495 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6496 capo: 0,
6497 }),
6498 }),
6499 &mut score,
6500 )
6501 .expect("tablature configuration should apply");
6502 stack
6503 .execute(
6504 Command::SetStaffPresentation(SetStaffPresentationCmd {
6505 part_index: 0,
6506 staff_index: 0,
6507 presentation: tab.clone(),
6508 }),
6509 &mut score,
6510 )
6511 .expect("staff presentation should apply");
6512 assert_eq!(stack.undo_key().as_deref(), Some("SetStaffPresentation"));
6513 assert_eq!(score.parts[0].staves[0].presentation, tab);
6514 stack
6515 .undo(&mut score)
6516 .expect("presentation undo should apply");
6517 assert_eq!(
6518 score.parts[0].staves[0].presentation.kind,
6519 StaffKind::Tablature
6520 );
6521 stack
6522 .redo(&mut score)
6523 .expect("presentation redo should apply");
6524 assert_eq!(
6525 score.parts[0].staves[0].presentation.kind,
6526 StaffKind::Tablature
6527 );
6528 }
6529
6530 #[test]
6531 fn set_instrument_definition_is_undoable_and_rejects_invalid_ranges() {
6532 let mut stack = CommandStack::new(50);
6533 let mut score = default_engine_score();
6534 let mut invalid = InstrumentDefinition::new("violin", "Violin");
6535 invalid.written_range = Some(InstrumentRange {
6536 lowest: 100,
6537 highest: 55,
6538 });
6539 let before = score.clone();
6540 assert!(
6541 stack
6542 .execute(
6543 Command::SetInstrumentDefinition(SetInstrumentDefinitionCmd {
6544 part_index: 0,
6545 definition: Some(invalid),
6546 }),
6547 &mut score,
6548 )
6549 .is_err()
6550 );
6551 assert_eq!(score.parts[0].instrument, before.parts[0].instrument);
6552
6553 let mut definition = InstrumentDefinition::new("violin", "Violin");
6554 definition.short_name = "Vln.".to_string();
6555 definition.family = Some("strings".to_string());
6556 definition.written_range = Some(InstrumentRange {
6557 lowest: 55,
6558 highest: 103,
6559 });
6560 definition.sounding_range = definition.written_range;
6561 definition.default_clefs = vec![Clef::Treble];
6562 definition.midi_program = 40;
6563 let command = Command::SetInstrumentDefinition(SetInstrumentDefinitionCmd {
6564 part_index: 0,
6565 definition: Some(definition.clone()),
6566 });
6567 let json = serde_json::to_string(&command).expect("command should serialize");
6568 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6569 assert_eq!(command_key(&decoded), "SetInstrumentDefinition");
6570 stack
6571 .execute(decoded, &mut score)
6572 .expect("definition applies");
6573 assert_eq!(score.parts[0].instrument, Some(definition));
6574 stack.undo(&mut score).expect("definition undo applies");
6575 assert!(score.parts[0].instrument.is_none());
6576 stack.redo(&mut score).expect("definition redo applies");
6577 assert_eq!(
6578 score.parts[0]
6579 .instrument
6580 .as_ref()
6581 .map(|value| value.id.as_str()),
6582 Some("violin")
6583 );
6584 }
6585
6586 #[test]
6587 fn set_percussion_kit_is_undoable_and_rejects_invalid_entries() {
6588 let mut stack = CommandStack::new(50);
6589 let mut score = default_engine_score();
6590 let invalid = Command::SetPercussionKit(SetPercussionKitCmd {
6591 part_index: 0,
6592 instruments: vec![PercussionInstrument {
6593 id: "snare".to_string(),
6594 name: None,
6595 midi_unpitched: Some(38),
6596 staff_position: Some(33),
6597 notehead: None,
6598 preferred_voice: None,
6599 techniques: Vec::new(),
6600 }],
6601 });
6602 assert!(stack.execute(invalid, &mut score).is_err());
6603 assert!(score.parts[0].percussion_instruments.is_empty());
6604
6605 let kit = vec![PercussionInstrument {
6606 id: "snare".to_string(),
6607 name: Some("Acoustic Snare".to_string()),
6608 midi_unpitched: Some(38),
6609 staff_position: Some(0),
6610 notehead: Some(NoteHead::Cross),
6611 preferred_voice: Some(1),
6612 techniques: vec!["rim-shot".to_string()],
6613 }];
6614 let command = Command::SetPercussionKit(SetPercussionKitCmd {
6615 part_index: 0,
6616 instruments: kit.clone(),
6617 });
6618 let json = serde_json::to_string(&command).expect("command serializes");
6619 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6620 assert_eq!(command_key(&decoded), "SetPercussionKit");
6621 stack.execute(decoded, &mut score).expect("kit applies");
6622 assert_eq!(score.parts[0].percussion_instruments, kit);
6623 stack.undo(&mut score).expect("kit undo applies");
6624 assert!(score.parts[0].percussion_instruments.is_empty());
6625 stack.redo(&mut score).expect("kit redo applies");
6626 assert_eq!(score.parts[0].percussion_instruments, kit);
6627 }
6628
6629 #[test]
6630 fn set_measure_instrument_change_is_undoable_and_json_compatible() {
6631 let mut stack = CommandStack::new(50);
6632 let mut score = default_engine_score();
6633 let mut definition = InstrumentDefinition::new("clarinet-bb", "B-flat Clarinet");
6634 definition.transpose_semitones = -2;
6635 definition.midi_program = 71;
6636 let command = Command::SetMeasureInstrumentChange(SetMeasureInstrumentChangeCmd {
6637 part_index: 0,
6638 staff_index: 0,
6639 measure_index: 2,
6640 definition: Some(definition.clone()),
6641 });
6642 let json = serde_json::to_string(&command).expect("command should serialize");
6643 let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
6644 assert_eq!(command_key(&decoded), "SetMeasureInstrumentChange");
6645 stack.execute(decoded, &mut score).expect("change applies");
6646 assert_eq!(
6647 score.parts[0].staves[0].measures[2].instrument_change,
6648 Some(definition)
6649 );
6650 stack.undo(&mut score).expect("undo applies");
6651 assert!(
6652 score.parts[0].staves[0].measures[2]
6653 .instrument_change
6654 .is_none()
6655 );
6656 }
6657
6658 #[test]
6659 fn set_measure_tablature_change_is_undoable_and_uses_base_line_count() {
6660 let mut stack = CommandStack::new(50);
6661 let mut score = default_engine_score();
6662 let base = crate::TablatureConfig {
6663 lines: 6,
6664 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6665 capo: 0,
6666 };
6667 score.parts[0].staves[0].tablature = Some(base.clone());
6668 let mut changed = base;
6669 changed.tuning_midi[0] = 38;
6670 changed.capo = 2;
6671 let command = Command::SetMeasureTablatureChange(SetMeasureTablatureChangeCmd {
6672 part_index: 0,
6673 staff_index: 0,
6674 measure_index: 2,
6675 config: Some(changed.clone()),
6676 });
6677 let json = serde_json::to_string(&command).expect("command serializes");
6678 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
6679 assert_eq!(command_key(&decoded), "SetMeasureTablatureChange");
6680 stack.execute(decoded, &mut score).expect("change applies");
6681 assert_eq!(
6682 score.parts[0].staves[0].tablature_at(1),
6683 score.parts[0].staves[0].tablature
6684 );
6685 assert_eq!(score.parts[0].staves[0].tablature_at(2), Some(changed));
6686 stack.undo(&mut score).expect("change undo applies");
6687 assert!(
6688 score.parts[0].staves[0].measures[2]
6689 .tablature_change
6690 .is_none()
6691 );
6692
6693 let invalid = Command::SetMeasureTablatureChange(SetMeasureTablatureChangeCmd {
6694 part_index: 0,
6695 staff_index: 0,
6696 measure_index: 1,
6697 config: Some(crate::TablatureConfig {
6698 lines: 7,
6699 tuning_midi: vec![40, 45, 50, 55, 59, 64, 69],
6700 capo: 0,
6701 }),
6702 });
6703 assert!(stack.execute(invalid, &mut score).is_err());
6704 }
6705
6706 #[test]
6707 fn batch_execute_empty_is_noop() {
6708 let mut stack = CommandStack::new(50);
6709 let mut score = default_engine_score();
6710 stack.batch_execute(vec![], &mut score).unwrap();
6711 assert!(!stack.can_undo());
6712 }
6713
6714 #[test]
6717 fn batch_label_used_as_command_key() {
6718 let cmd = Command::Batch(BatchCmd {
6719 commands: vec![],
6720 label: Some("ApplyAI".to_string()),
6721 });
6722 assert_eq!(command_key(&cmd), "ApplyAI");
6723 }
6724
6725 #[test]
6726 fn batch_no_label_key_is_batch() {
6727 let cmd = Command::Batch(BatchCmd {
6728 commands: vec![],
6729 label: None,
6730 });
6731 assert_eq!(command_key(&cmd), "Batch");
6732 }
6733
6734 #[test]
6735 fn batch_label_survives_json_roundtrip() {
6736 let cmd = Command::Batch(BatchCmd {
6737 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 120 })],
6738 label: Some("PasteSelection".to_string()),
6739 });
6740 let json = serde_json::to_string(&cmd).unwrap();
6741 let cmd2: Command = serde_json::from_str(&json).unwrap();
6742 assert_eq!(command_key(&cmd2), "PasteSelection");
6743 }
6744
6745 #[test]
6746 fn batch_label_in_undo_key() {
6747 let mut stack = CommandStack::new(50);
6748 let mut score = default_engine_score();
6749 let cmd = Command::Batch(BatchCmd {
6750 commands: vec![Command::SetTempo(SetTempoCmd { bpm: 140 })],
6751 label: Some("ApplyAI".to_string()),
6752 });
6753 stack.execute(cmd, &mut score).unwrap();
6754 assert_eq!(stack.undo_key(), Some("ApplyAI".to_string()));
6755 }
6756
6757 #[test]
6758 fn undo_returns_change_hint() {
6759 use crate::model::change_hint::ChangeScope;
6760 let mut stack = CommandStack::new(50);
6761 let mut score = default_engine_score();
6762 stack
6763 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
6764 .unwrap();
6765 let hint = stack.undo(&mut score).unwrap();
6766 assert_eq!(hint.scope, ChangeScope::Global);
6767 assert!(hint.playback_dirty);
6768 }
6769
6770 #[test]
6771 fn redo_returns_change_hint() {
6772 use crate::model::change_hint::ChangeScope;
6773 let mut stack = CommandStack::new(50);
6774 let mut score = default_engine_score();
6775 stack
6776 .execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
6777 .unwrap();
6778 stack.undo(&mut score).unwrap();
6779 let hint = stack.redo(&mut score).unwrap();
6780 assert_eq!(hint.scope, ChangeScope::Global);
6781 assert!(hint.playback_dirty);
6782 }
6783
6784 #[test]
6787 fn toggle_slur_sets_start_and_end() {
6788 let mut score = default_engine_score();
6789 let cmd = Command::AddNote(AddNoteCmd {
6790 part_index: 0,
6791 staff_index: 0,
6792 measure_index: 0,
6793 voice: 0,
6794 position: 0,
6795 pitch: Some(Pitch::new(Step::C, 4)),
6796 duration: Duration::Quarter,
6797 dot_count: 0,
6798 is_rest: false,
6799 tuplet: None,
6800 });
6801 apply_command(&cmd, &mut score).unwrap();
6802 apply_command(
6803 &Command::AddNote(AddNoteCmd {
6804 part_index: 0,
6805 staff_index: 0,
6806 measure_index: 0,
6807 voice: 0,
6808 position: 1,
6809 pitch: Some(Pitch::new(Step::D, 4)),
6810 duration: Duration::Quarter,
6811 dot_count: 0,
6812 is_rest: false,
6813 tuplet: None,
6814 }),
6815 &mut score,
6816 )
6817 .unwrap();
6818 let start = NoteAddr {
6819 part: 0,
6820 staff: 0,
6821 measure: 0,
6822 voice: 0,
6823 note: 0,
6824 };
6825 let end = NoteAddr {
6826 part: 0,
6827 staff: 0,
6828 measure: 0,
6829 voice: 0,
6830 note: 1,
6831 };
6832 apply_command(
6833 &Command::ToggleSlur(ToggleSlurCmd {
6834 start: start.clone(),
6835 end: end.clone(),
6836 }),
6837 &mut score,
6838 )
6839 .unwrap();
6840 assert!(score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
6841 assert!(score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
6842 apply_command(
6844 &Command::ToggleSlur(ToggleSlurCmd { start, end }),
6845 &mut score,
6846 )
6847 .unwrap();
6848 assert!(!score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
6849 assert!(!score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
6850 }
6851
6852 #[test]
6855 fn add_staff_appends_staff_with_correct_measure_count() {
6856 let mut score = default_engine_score();
6857 let before = score.parts[0].staves.len();
6858 let measure_count = score.parts[0].staves[0].measures.len();
6859 apply_command(
6860 &Command::AddStaff(AddStaffCmd {
6861 part_index: 0,
6862 clef: Clef::Bass,
6863 }),
6864 &mut score,
6865 )
6866 .unwrap();
6867 assert_eq!(score.parts[0].staves.len(), before + 1);
6868 let new_staff = score.parts[0].staves.last().unwrap();
6869 assert_eq!(new_staff.measures.len(), measure_count);
6870 }
6871
6872 #[test]
6873 fn add_staff_out_of_range_returns_err() {
6874 let mut score = default_engine_score();
6875 let result = apply_command(
6876 &Command::AddStaff(AddStaffCmd {
6877 part_index: 99,
6878 clef: Clef::Treble,
6879 }),
6880 &mut score,
6881 );
6882 assert!(result.is_err());
6883 }
6884
6885 #[test]
6886 fn delete_staff_removes_extra_staff() {
6887 let mut score = default_engine_score();
6888 apply_command(
6889 &Command::AddStaff(AddStaffCmd {
6890 part_index: 0,
6891 clef: Clef::Bass,
6892 }),
6893 &mut score,
6894 )
6895 .unwrap();
6896 assert_eq!(score.parts[0].staves.len(), 2);
6897 apply_command(
6898 &Command::DeleteStaff(DeleteStaffCmd {
6899 part_index: 0,
6900 staff_index: 1,
6901 }),
6902 &mut score,
6903 )
6904 .unwrap();
6905 assert_eq!(score.parts[0].staves.len(), 1);
6906 }
6907
6908 #[test]
6909 fn delete_last_staff_returns_err() {
6910 let mut score = default_engine_score();
6911 assert_eq!(score.parts[0].staves.len(), 1);
6912 let result = apply_command(
6913 &Command::DeleteStaff(DeleteStaffCmd {
6914 part_index: 0,
6915 staff_index: 0,
6916 }),
6917 &mut score,
6918 );
6919 assert!(result.is_err());
6920 }
6921
6922 #[test]
6925 fn set_tuplet_assigns_and_clears() {
6926 use crate::model::notation::TupletInfo;
6927 let mut score = default_engine_score();
6928 apply_command(
6929 &Command::AddNote(AddNoteCmd {
6930 part_index: 0,
6931 staff_index: 0,
6932 measure_index: 0,
6933 voice: 0,
6934 position: 0,
6935 pitch: Some(Pitch::new(Step::C, 4)),
6936 duration: Duration::Quarter,
6937 dot_count: 0,
6938 is_rest: false,
6939 tuplet: None,
6940 }),
6941 &mut score,
6942 )
6943 .unwrap();
6944 let ti = TupletInfo {
6945 actual_notes: 3,
6946 normal_notes: 2,
6947 };
6948 apply_command(
6949 &Command::SetTuplet(SetTupletCmd {
6950 part_index: 0,
6951 staff_index: 0,
6952 measure_index: 0,
6953 voice_index: 0,
6954 note_index: 0,
6955 tuplet: Some(ti.clone()),
6956 }),
6957 &mut score,
6958 )
6959 .unwrap();
6960 assert_eq!(
6961 score.parts[0].staves[0].measures[0].voices[0][0].tuplet,
6962 Some(ti)
6963 );
6964 apply_command(
6965 &Command::SetTuplet(SetTupletCmd {
6966 part_index: 0,
6967 staff_index: 0,
6968 measure_index: 0,
6969 voice_index: 0,
6970 note_index: 0,
6971 tuplet: None,
6972 }),
6973 &mut score,
6974 )
6975 .unwrap();
6976 assert!(
6977 score.parts[0].staves[0].measures[0].voices[0][0]
6978 .tuplet
6979 .is_none()
6980 );
6981 }
6982
6983 #[test]
6986 fn respell_score_cmd_changes_all_pitches() {
6987 use crate::model::pitch::Step;
6988 let mut score = default_engine_score();
6989 apply_command(
6990 &Command::AddNote(AddNoteCmd {
6991 part_index: 0,
6992 staff_index: 0,
6993 measure_index: 0,
6994 voice: 0,
6995 position: 0,
6996 pitch: Some(Pitch::with_alter(Step::C, 4, 1)), duration: Duration::Quarter,
6998 dot_count: 0,
6999 is_rest: false,
7000 tuplet: None,
7001 }),
7002 &mut score,
7003 )
7004 .unwrap();
7005 apply_command(
7006 &Command::RespellScore(RespellScoreCmd { prefer_flat: true }),
7007 &mut score,
7008 )
7009 .unwrap();
7010 let pitch = &score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
7011 assert_eq!(pitch.step, Step::D);
7012 assert_eq!(pitch.alter, -1); }
7014
7015 fn respell_fixture(measures: u32) -> Score {
7016 use crate::model::pitch::Step;
7017 let mut score = Score::new("Respell", 120, 4, 4, 0, measures);
7018 for measure in &mut score.parts[0].staves[0].measures {
7019 measure.voices[0] = vec![Note::new(Pitch::with_alter(Step::C, 4, 1), Duration::Whole)];
7020 }
7021 score
7022 }
7023
7024 fn spelled(score: &Score, measure: usize) -> (crate::model::pitch::Step, i8) {
7025 let pitch = &score.parts[0].staves[0].measures[measure].voices[0][0].pitches[0];
7026 (pitch.step.clone(), pitch.alter)
7027 }
7028
7029 #[test]
7030 fn respell_staff_region_changes_only_the_selection_and_is_undoable() {
7031 use crate::model::pitch::Step;
7032 let mut score = respell_fixture(4);
7033 let mut stack = CommandStack::new(50);
7034 let command = Command::RespellStaffRegion(RespellStaffRegionCmd {
7035 part_index: 0,
7036 staff_index: 0,
7037 start_measure: 1,
7038 end_measure: 3,
7039 policy: RespellPolicy::Flat,
7040 });
7041 let json = serde_json::to_string(&command).expect("command serializes");
7042 assert!(json.contains("\"policy\":\"flat\""));
7043 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7044 assert_eq!(command_key(&decoded), "RespellStaffRegion");
7045 assert_eq!(command_label(&decoded), "Respell Pitches (flat)");
7046 stack.execute(decoded, &mut score).expect("region respells");
7047 assert_eq!(spelled(&score, 0), (Step::C, 1));
7048 assert_eq!(spelled(&score, 1), (Step::D, -1));
7049 assert_eq!(spelled(&score, 2), (Step::D, -1));
7050 assert_eq!(spelled(&score, 3), (Step::C, 1));
7051 stack.undo(&mut score).expect("region respell undoes");
7052 assert!((0..4).all(|measure| spelled(&score, measure) == (Step::C, 1)));
7053 stack.redo(&mut score).expect("region respell redoes");
7054 assert_eq!(spelled(&score, 2), (Step::D, -1));
7055 }
7056
7057 #[test]
7058 fn respell_staff_region_keeps_ties_across_the_boundary_in_one_spelling() {
7059 use crate::model::pitch::Step;
7060 let mut score = respell_fixture(4);
7061 {
7062 let measures = &mut score.parts[0].staves[0].measures;
7063 measures[0].voices[0][0].tie_start = true;
7064 measures[1].voices[0][0].tie_end = true;
7065 measures[1].voices[0][0].tie_start = true;
7066 measures[2].voices[0][0].tie_end = true;
7067 }
7068 let changed = respell_staff_region(&mut score, 0, 0, 1, 2, RespellPolicy::Flat)
7069 .expect("tied region respells");
7070 assert_eq!(changed, (0, 2));
7071 assert_eq!(spelled(&score, 0), (Step::D, -1));
7072 assert_eq!(spelled(&score, 1), (Step::D, -1));
7073 assert_eq!(spelled(&score, 2), (Step::D, -1));
7074 assert_eq!(spelled(&score, 3), (Step::C, 1));
7075 }
7076
7077 #[test]
7078 fn respell_staff_region_follows_local_keys_and_skips_unpitched_notes() {
7079 use crate::model::notation::KeySignature;
7080 use crate::model::pitch::Step;
7081 let mut score = respell_fixture(4);
7082 for measure in &mut score.parts[0].staves[0].measures {
7083 measure.voices[0][0].pitches[0] = Pitch::with_alter(Step::D, 4, -1);
7084 }
7085 score.parts[0].staves[0].measures[2].key_sig = Some(KeySignature {
7086 fifths: -3,
7087 mode: "major".into(),
7088 });
7089 score.parts[0].staves[0].measures[1].voices[0][0].is_unpitched = true;
7090 respell_staff_region(&mut score, 0, 0, 0, 4, RespellPolicy::Key).expect("key respell");
7091 assert_eq!(spelled(&score, 0), (Step::C, 1));
7092 assert_eq!(
7093 spelled(&score, 1),
7094 (Step::D, -1),
7095 "unpitched staff position is kept"
7096 );
7097 assert_eq!(spelled(&score, 2), (Step::D, -1));
7098 assert_eq!(spelled(&score, 3), (Step::D, -1));
7099
7100 assert!(matches!(
7101 respell_staff_region(&mut score, 0, 0, 2, 2, RespellPolicy::Sharp),
7102 Err(Error::InvalidCommand(_))
7103 ));
7104 assert!(matches!(
7105 respell_staff_region(&mut score, 0, 5, 0, 1, RespellPolicy::Sharp),
7106 Err(Error::StaffNotFound(5))
7107 ));
7108 assert!(matches!(
7109 respell_staff_region(&mut score, 0, 0, 0, 5, RespellPolicy::Sharp),
7110 Err(Error::InvalidCommand(_))
7111 ));
7112 }
7113
7114 #[test]
7115 fn pickup_measure_length_limits_editing_and_survives_time_signature_changes() {
7116 use crate::model::score::MeasureLength;
7117 let mut score = Score::new("Pickup", 120, 4, 4, 0, 2);
7118 score.parts[0].staves[0].measures[0].actual_length = Some(MeasureLength {
7119 numerator: 1,
7120 denominator: 4,
7121 });
7122 score.parts[0].staves[0].measures[0].voices[0] = vec![Note::rest(Duration::Quarter)];
7123 assert!(validate(&score).is_valid());
7124 assert_eq!(
7125 crate::measure_beats_remaining(&score, 0, 0, 0, 0).expect("capacity"),
7126 0.0
7127 );
7128 let mut stack = CommandStack::new(50);
7129 stack
7130 .execute(
7131 Command::AddNote(AddNoteCmd {
7132 part_index: 0,
7133 staff_index: 0,
7134 measure_index: 0,
7135 voice: 0,
7136 position: 0,
7137 pitch: Some(Pitch::new(Step::G, 4)),
7138 duration: Duration::Quarter,
7139 dot_count: 0,
7140 is_rest: false,
7141 tuplet: None,
7142 }),
7143 &mut score,
7144 )
7145 .expect("note fits the pickup after trimming");
7146 let pickup = &score.parts[0].staves[0].measures[0].voices[0];
7147 assert_eq!(pickup.len(), 1);
7148 assert!(!pickup[0].is_rest);
7149
7150 stack
7151 .execute(
7152 Command::SetTimeSignature(SetTimeSignatureCmd {
7153 numerator: 3,
7154 denominator: 4,
7155 }),
7156 &mut score,
7157 )
7158 .expect("time signature changes");
7159 let beats: f64 = score.parts[0].staves[0].measures[0].voices[0]
7160 .iter()
7161 .map(|note| note.beats())
7162 .sum();
7163 assert!((beats - 1.0).abs() < 1e-9, "the pickup keeps one beat");
7164
7165 score.parts[0].staves[0].measures[1].actual_length = Some(MeasureLength {
7166 numerator: 0,
7167 denominator: 4,
7168 });
7169 assert!(validate(&score).errors.iter().any(|error| matches!(
7170 error,
7171 crate::ValidationError::InvalidMeasureLength { measure: 1, .. }
7172 )));
7173 }
7174
7175 #[test]
7176 fn set_lyric_addresses_verses_without_touching_verse_one() {
7177 use crate::model::notation::VerseLyric;
7178 let mut score = Score::new("Verses", 120, 4, 4, 0, 1);
7179 score.parts[0].staves[0].measures[0].voices[0] =
7180 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
7181 let lyric = |text: &str| Lyric {
7182 text: text.into(),
7183 syllabic: "single".into(),
7184 extend: false,
7185 };
7186 let set = |verse: Option<u8>, text: Option<&str>| {
7187 Command::SetLyric(SetLyricCmd {
7188 part_index: 0,
7189 staff_index: 0,
7190 measure_index: 0,
7191 voice: 0,
7192 note_index: 0,
7193 lyric: text.map(lyric),
7194 verse,
7195 })
7196 };
7197 let mut stack = CommandStack::new(50);
7198 stack
7199 .execute(set(None, Some("one")), &mut score)
7200 .expect("verse 1");
7201 stack
7202 .execute(set(Some(3), Some("three")), &mut score)
7203 .expect("verse 3");
7204 stack
7205 .execute(set(Some(2), Some("two")), &mut score)
7206 .expect("verse 2");
7207 stack
7208 .execute(set(Some(2), Some("TWO")), &mut score)
7209 .expect("replace verse 2");
7210 let note = |score: &Score| score.parts[0].staves[0].measures[0].voices[0][0].clone();
7211 assert_eq!(note(&score).lyric, Some(lyric("one")));
7212 assert_eq!(
7213 note(&score).additional_lyrics,
7214 vec![
7215 VerseLyric {
7216 verse: 2,
7217 lyric: lyric("TWO")
7218 },
7219 VerseLyric {
7220 verse: 3,
7221 lyric: lyric("three")
7222 }
7223 ]
7224 );
7225 stack
7226 .execute(set(Some(3), None), &mut score)
7227 .expect("clear verse 3");
7228 assert_eq!(note(&score).additional_lyrics.len(), 1);
7229 stack.undo(&mut score).expect("clear undoes");
7230 assert_eq!(note(&score).additional_lyrics.len(), 2);
7231
7232 let current = serde_json::to_string(&set(None, None)).expect("SetLyric serializes");
7234 assert!(current.contains("\"verse\":null"));
7235 let legacy: Command = serde_json::from_str(¤t.replace(",\"verse\":null", ""))
7236 .expect("legacy SetLyric JSON");
7237 stack
7238 .execute(legacy, &mut score)
7239 .expect("legacy clears verse 1");
7240 assert_eq!(note(&score).lyric, None);
7241 assert_eq!(note(&score).additional_lyrics.len(), 2);
7242
7243 assert!(matches!(
7244 apply_command(&set(Some(0), Some("zero")), &mut score),
7245 Err(Error::InvalidCommand(_))
7246 ));
7247 assert!(matches!(
7248 apply_command(&set(Some(33), Some("too far")), &mut score),
7249 Err(Error::InvalidCommand(_))
7250 ));
7251 score.parts[0].staves[0].measures[0].voices[0][0]
7252 .additional_lyrics
7253 .push(VerseLyric {
7254 verse: 2,
7255 lyric: lyric("duplicate"),
7256 });
7257 assert!(validate(&score).errors.iter().any(|error| matches!(
7258 error,
7259 crate::ValidationError::InvalidLyricVerse { verse: 2, .. }
7260 )));
7261 }
7262
7263 #[test]
7264 fn cycle_enharmonic_spelling_targets_one_chord_member_and_undoes() {
7265 let mut score = Score::new("Enharmonic", 120, 4, 4, 0, 1);
7266 let mut chord = Note::new(Pitch::with_alter(Step::C, 4, 1), Duration::Whole);
7267 chord.pitches.push(Pitch::with_alter(Step::G, 4, 1));
7268 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
7269 let mut stack = CommandStack::new(50);
7270 let command = Command::CycleEnharmonicSpelling(CycleEnharmonicSpellingCmd {
7271 part_index: 0,
7272 staff_index: 0,
7273 measure_index: 0,
7274 voice: 0,
7275 note_index: 0,
7276 pitch_index: Some(1),
7277 });
7278 let json = serde_json::to_string(&command).expect("command serializes");
7279 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7280 assert_eq!(command_key(&decoded), "CycleEnharmonicSpelling");
7281 stack.execute(decoded, &mut score).expect("spelling cycles");
7282 let pitches = |score: &Score| {
7283 score.parts[0].staves[0].measures[0].voices[0][0]
7284 .pitches
7285 .clone()
7286 };
7287 assert_eq!(
7288 pitches(&score),
7289 vec![
7290 Pitch::with_alter(Step::C, 4, 1),
7291 Pitch::with_alter(Step::A, 4, -1)
7292 ]
7293 );
7294 stack.undo(&mut score).expect("spelling undoes");
7295 assert_eq!(pitches(&score)[1], Pitch::with_alter(Step::G, 4, 1));
7296
7297 let mut rest_score = Score::new("Rest", 120, 4, 4, 0, 1);
7298 rest_score.parts[0].staves[0].measures[0].voices[0] = vec![Note::rest(Duration::Whole)];
7299 assert!(matches!(
7300 apply_command(&command, &mut rest_score),
7301 Err(Error::InvalidCommand(_))
7302 ));
7303 }
7304
7305 #[test]
7306 fn resequence_rehearsal_marks_continues_the_first_sequence() {
7307 let mut score = Score::new("Marks", 120, 4, 4, 0, 30);
7308 let mut stack = CommandStack::new(50);
7309 for (index, text) in [(0, "A"), (4, "C"), (9, "C"), (12, "Q")] {
7310 for_each_measure_at(&mut score, index, |measure| {
7311 measure.rehearsal = Some(text.into())
7312 });
7313 }
7314 let command = Command::ResequenceRehearsalMarks(ResequenceRehearsalMarksCmd::default());
7315 let json = serde_json::to_string(&command).expect("command serializes");
7316 let decoded: Command = serde_json::from_str(&json).expect("command deserializes");
7317 assert_eq!(command_key(&decoded), "ResequenceRehearsalMarks");
7318 stack
7319 .execute(decoded, &mut score)
7320 .expect("marks resequence");
7321 let marks = |score: &Score| -> Vec<Option<String>> {
7322 [0, 4, 9, 12]
7323 .iter()
7324 .map(|&index| score.parts[0].staves[0].measures[index].rehearsal.clone())
7325 .collect()
7326 };
7327 assert_eq!(
7328 marks(&score),
7329 ["A", "B", "C", "D"].map(|text| Some(text.to_string()))
7330 );
7331 stack.undo(&mut score).expect("resequence undoes");
7332 assert_eq!(marks(&score)[1].as_deref(), Some("C"));
7333
7334 let sequence = RehearsalSequence::detect("Z", 1).expect("letters");
7335 assert_eq!(sequence.nth(1, 1), "AA");
7336 assert_eq!(sequence.nth(2, 1), "AB");
7337 let lower = RehearsalSequence::detect("y", 1).expect("lower letters");
7338 assert_eq!(lower.nth(2, 1), "aa");
7339 assert_eq!(
7340 RehearsalSequence::detect("7", 3)
7341 .expect("numbers")
7342 .nth(2, 9),
7343 "9"
7344 );
7345 assert_eq!(
7346 RehearsalSequence::detect("5", 5)
7347 .expect("measure numbers")
7348 .nth(3, 17),
7349 "17"
7350 );
7351 assert!(RehearsalSequence::detect("Intro", 1).is_none());
7352
7353 for_each_measure_at(&mut score, 0, |measure| {
7354 measure.rehearsal = Some("Intro".into())
7355 });
7356 assert!(matches!(
7357 apply_command(&command, &mut score),
7358 Err(Error::InvalidCommand(_))
7359 ));
7360 }
7361
7362 #[test]
7363 fn system_break_interval_replaces_line_breaks_only() {
7364 let mut score = Score::new("Breaks", 120, 4, 4, 0, 10);
7365 score.parts[0].staves[0].measures[1].system_break = true;
7366 score.parts[0].staves[0].measures[2].page_break = true;
7367 apply_command(
7368 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7369 interval: 4,
7370 start_measure: None,
7371 end_measure: None,
7372 }),
7373 &mut score,
7374 )
7375 .expect("breaks every four measures");
7376 let breaks: Vec<usize> = score.parts[0].staves[0]
7377 .measures
7378 .iter()
7379 .enumerate()
7380 .filter(|(_, measure)| measure.system_break)
7381 .map(|(index, _)| index)
7382 .collect();
7383 assert_eq!(breaks, vec![3, 7]);
7384 assert!(score.parts[0].staves[0].measures[2].page_break);
7385
7386 let mut exact = Score::new("Exact", 120, 4, 4, 0, 8);
7387 apply_command(
7388 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7389 interval: 4,
7390 start_measure: None,
7391 end_measure: None,
7392 }),
7393 &mut exact,
7394 )
7395 .expect("no break after the final measure");
7396 assert!(!exact.parts[0].staves[0].measures[7].system_break);
7397 assert!(exact.parts[0].staves[0].measures[3].system_break);
7398
7399 apply_command(
7400 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7401 interval: 0,
7402 start_measure: Some(0),
7403 end_measure: Some(6),
7404 }),
7405 &mut score,
7406 )
7407 .expect("breaks removed in range");
7408 assert!(!score.parts[0].staves[0].measures[3].system_break);
7409 assert!(score.parts[0].staves[0].measures[7].system_break);
7410 assert!(matches!(
7411 apply_command(
7412 &Command::SetSystemBreakInterval(SetSystemBreakIntervalCmd {
7413 interval: 2,
7414 start_measure: Some(4),
7415 end_measure: Some(11),
7416 }),
7417 &mut score,
7418 ),
7419 Err(Error::InvalidCommand(_))
7420 ));
7421 }
7422
7423 #[test]
7424 fn remove_trailing_empty_measures_keeps_content_and_final_barline() {
7425 let mut score = Score::new("Trailing", 120, 4, 4, 0, 6);
7426 let mut stack = CommandStack::new(50);
7427 score.parts[0].staves[0].measures[1].voices[0] =
7428 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
7429 score.parts[0].staves[0].measures[5].barline_right = Barline::Final;
7430 let mut fermata_rest = Note::rest(Duration::Whole);
7431 fermata_rest.articulations = vec![Articulation::Fermata];
7432 score.parts[0].staves[0].measures[3].voices[0] = vec![fermata_rest];
7433 stack
7434 .execute(
7435 Command::RemoveTrailingEmptyMeasures(RemoveTrailingEmptyMeasuresCmd {}),
7436 &mut score,
7437 )
7438 .expect("trailing measures removed");
7439 let staff = &score.parts[0].staves[0];
7440 assert_eq!(staff.measures.len(), 4, "a rest with a fermata is content");
7441 assert!(matches!(staff.measures[3].barline_right, Barline::Final));
7442 assert_eq!(staff.measures[3].number, 4);
7443 stack.undo(&mut score).expect("removal undoes");
7444 assert_eq!(score.parts[0].staves[0].measures.len(), 6);
7445
7446 let mut empty = Score::new("Empty", 120, 4, 4, 0, 3);
7447 apply_command(
7448 &Command::RemoveTrailingEmptyMeasures(RemoveTrailingEmptyMeasuresCmd {}),
7449 &mut empty,
7450 )
7451 .expect("empty score keeps one measure");
7452 assert_eq!(empty.parts[0].staves[0].measures.len(), 1);
7453 }
7454
7455 #[test]
7456 fn typed_spanner_commands_are_atomic_and_undoable() {
7457 use crate::model::score::{NotationSpanner, NotationSpannerKind};
7458
7459 let mut score = default_engine_score();
7460 let address = NoteAddr {
7461 part: 0,
7462 staff: 0,
7463 measure: 0,
7464 voice: 0,
7465 note: 0,
7466 };
7467 let spanner = NotationSpanner {
7468 id: "slur-1".to_string(),
7469 kind: NotationSpannerKind::Slur,
7470 start: address.clone(),
7471 end: address,
7472 number: Some(1),
7473 line_type: Some("dashed".to_string()),
7474 text: None,
7475 placement: Some("above".to_string()),
7476 ottava_size: None,
7477 ottava_type: None,
7478 };
7479 let mut stack = CommandStack::new(8);
7480 stack
7481 .execute(Command::AddSpanner(AddSpannerCmd { spanner }), &mut score)
7482 .unwrap();
7483 assert_eq!(score.spanners.len(), 1);
7484 assert_eq!(stack.undo_key().as_deref(), Some("AddSpanner"));
7485
7486 let before_duplicate = score.clone();
7487 let duplicate = score.spanners[0].clone();
7488 assert!(
7489 stack
7490 .execute(
7491 Command::AddSpanner(AddSpannerCmd { spanner: duplicate }),
7492 &mut score,
7493 )
7494 .is_err()
7495 );
7496 assert_eq!(score.spanners, before_duplicate.spanners);
7497
7498 let mut updated = score.spanners[0].clone();
7499 updated.number = Some(2);
7500 stack
7501 .execute(
7502 Command::UpdateSpanner(UpdateSpannerCmd { spanner: updated }),
7503 &mut score,
7504 )
7505 .unwrap();
7506 assert_eq!(score.spanners[0].number, Some(2));
7507 stack.undo(&mut score).unwrap();
7508 assert_eq!(score.spanners[0].number, Some(1));
7509 stack.redo(&mut score).unwrap();
7510 assert_eq!(score.spanners[0].number, Some(2));
7511
7512 stack
7513 .execute(
7514 Command::RemoveSpanner(RemoveSpannerCmd {
7515 id: "slur-1".to_string(),
7516 }),
7517 &mut score,
7518 )
7519 .unwrap();
7520 assert!(score.spanners.is_empty());
7521 stack.undo(&mut score).unwrap();
7522 assert_eq!(score.spanners.len(), 1);
7523 }
7524
7525 #[test]
7526 fn string_quartet_linked_views_roundtrip_and_undo_redo_as_one_contract() {
7527 let mut score = Score::template(ScoreTemplate::StringQuartet);
7528 let mut stack = CommandStack::new(16);
7529 for (part, id, name) in [
7530 (0, "violin-1", "Violin I"),
7531 (1, "violin-2", "Violin II"),
7532 (2, "viola", "Viola"),
7533 (3, "cello", "Cello"),
7534 ] {
7535 stack
7536 .execute(
7537 Command::UpsertScoreView(UpsertScoreViewCmd {
7538 view: ScoreView::linked_part(id, name, part),
7539 }),
7540 &mut score,
7541 )
7542 .expect("linked part view applies");
7543 }
7544 assert_eq!(score.parts.len(), 4);
7545 assert_eq!(score.views.len(), 4);
7546 for (part, view) in score.views.iter().enumerate() {
7547 assert_eq!(view.parts, vec![part]);
7548 assert_eq!(score.resolve_view(&view.id).unwrap().parts.len(), 1);
7549 }
7550
7551 let restored: Score =
7552 serde_json::from_str(&serde_json::to_string(&score).expect("quartet score serializes"))
7553 .expect("quartet score deserializes");
7554 assert_eq!(restored.views, score.views);
7555
7556 for _ in 0..4 {
7557 stack.undo(&mut score).expect("view undo applies");
7558 }
7559 assert!(score.views.is_empty());
7560 for _ in 0..4 {
7561 stack.redo(&mut score).expect("view redo applies");
7562 }
7563 assert_eq!(score.views, restored.views);
7564 }
7565
7566 #[test]
7567 fn linked_view_rejects_invalid_projection_and_keeps_the_source_intact() {
7568 use crate::model::score::{ViewStaffKindOverride, ViewStaffRef, ViewStyleProperty};
7569
7570 let mut score = Score::template(ScoreTemplate::StringQuartet);
7571 let original = score.clone();
7572 let original_parts = serde_json::to_value(&score.parts).unwrap();
7573 let base = ScoreView::linked_part("viola", "Viola", 2);
7574 let mut invalid = base.clone();
7575 invalid.parts.push(2);
7576 assert!(
7577 apply_command(
7578 &Command::UpsertScoreView(UpsertScoreViewCmd { view: invalid }),
7579 &mut score
7580 )
7581 .is_err()
7582 );
7583 let mut invalid = base.clone();
7584 invalid
7585 .layout
7586 .hidden_staves
7587 .push(ViewStaffRef { part: 1, staff: 0 });
7588 assert!(
7589 apply_command(
7590 &Command::UpsertScoreView(UpsertScoreViewCmd { view: invalid }),
7591 &mut score
7592 )
7593 .is_err()
7594 );
7595 let mut invalid = base.clone();
7596 invalid.staff_kind_overrides.push(ViewStaffKindOverride {
7597 staff: ViewStaffRef { part: 2, staff: 0 },
7598 kind: StaffKind::Tablature,
7599 });
7600 assert!(
7601 apply_command(
7602 &Command::UpsertScoreView(UpsertScoreViewCmd { view: invalid }),
7603 &mut score
7604 )
7605 .is_err()
7606 );
7607 let mut invalid = base.clone();
7608 invalid
7609 .layout
7610 .typed_style_overrides
7611 .push(ViewStyleOverride {
7612 property: ViewStyleProperty::StaffSpace,
7613 value: f32::NAN,
7614 });
7615 assert!(
7616 apply_command(
7617 &Command::UpsertScoreView(UpsertScoreViewCmd { view: invalid }),
7618 &mut score
7619 )
7620 .is_err()
7621 );
7622 assert_eq!(score.views, original.views);
7623 assert_eq!(serde_json::to_value(&score.parts).unwrap(), original_parts);
7624
7625 let mut view = base;
7626 view.parts = vec![2, 0];
7627 view.layout
7628 .hidden_staves
7629 .push(ViewStaffRef { part: 0, staff: 0 });
7630 view.layout.typed_style_overrides.push(ViewStyleOverride {
7631 property: ViewStyleProperty::StaffSpace,
7632 value: 1.25,
7633 });
7634 view.staff_kind_overrides.push(ViewStaffKindOverride {
7635 staff: ViewStaffRef { part: 2, staff: 0 },
7636 kind: StaffKind::Percussion,
7637 });
7638 let mut stack = CommandStack::new(4);
7639 stack
7640 .execute(
7641 Command::UpsertScoreView(UpsertScoreViewCmd { view }),
7642 &mut score,
7643 )
7644 .expect("valid linked projection applies");
7645 let projected = score.resolve_view("viola").expect("view resolves");
7646 assert_eq!(projected.parts.len(), 2);
7647 assert_eq!(projected.parts[0].name, original.parts[2].name);
7648 assert_eq!(projected.parts[1].name, original.parts[0].name);
7649 assert_eq!(
7650 projected.parts[0].staves[0].presentation.kind,
7651 StaffKind::Percussion
7652 );
7653 assert_eq!(projected.views[0].layout.hidden_staves[0].part, 1);
7654 assert_eq!(projected.views[0].layout.resolved_style().staff_space, 1.25);
7655 assert_eq!(serde_json::to_value(&score.parts).unwrap(), original_parts);
7656 stack.undo(&mut score).expect("view creation is undoable");
7657 assert!(score.views.is_empty());
7658 }
7659
7660 #[test]
7661 fn template_score_edit_session_keeps_global_and_measure_marks_in_sync() {
7662 let mut score = Score::default();
7663 apply_command(
7664 &Command::NewScore(NewScoreCmd {
7665 title: "Quartet rehearsal".into(),
7666 composer: "Composer".into(),
7667 tempo_bpm: 96,
7668 time_numerator: 3,
7669 time_denominator: 4,
7670 key_fifths: 2,
7671 measure_count: 2,
7672 template: Some(ScoreTemplate::StringQuartet),
7673 }),
7674 &mut score,
7675 )
7676 .unwrap();
7677 assert_eq!(score.parts.len(), 4);
7678 assert_eq!(score.parts[3].staves[0].measures.len(), 2);
7679 assert_eq!(score.metadata.composer, "Composer");
7680 for command in [
7681 Command::SetKeySignature(SetKeySignatureCmd { fifths: -1 }),
7682 Command::SetRehearsalMark(SetRehearsalMarkCmd {
7683 measure_index: 0,
7684 text: Some("A".into()),
7685 }),
7686 Command::SetNavigationMark(SetNavigationMarkCmd {
7687 measure_index: 1,
7688 mark: Some("Fine".into()),
7689 }),
7690 Command::SetSystemBreak(SetSystemBreakCmd {
7691 measure_index: 1,
7692 value: true,
7693 }),
7694 Command::SetPageBreak(SetPageBreakCmd {
7695 measure_index: 1,
7696 value: true,
7697 }),
7698 ] {
7699 apply_command(&command, &mut score).unwrap();
7700 }
7701 assert_eq!(score.settings.key_signature.fifths, -1);
7702 for part in &score.parts {
7703 let measures = &part.staves[0].measures;
7704 assert_eq!(measures[0].rehearsal.as_deref(), Some("A"));
7705 assert_eq!(measures[1].navigation.as_deref(), Some("Fine"));
7706 assert!(measures[1].system_break);
7707 assert!(measures[1].page_break);
7708 }
7709 }
7710
7711 #[test]
7712 fn section_break_is_atomic_undoable_and_synced_across_staves() {
7713 let mut score = Score::template(ScoreTemplate::Piano);
7714 let original = score.clone();
7715 let mut stack = CommandStack::new(4);
7716 stack
7717 .execute(
7718 Command::SetSectionBreak(SetSectionBreakCmd {
7719 measure_index: 2,
7720 value: true,
7721 }),
7722 &mut score,
7723 )
7724 .expect("section break applies");
7725 assert!(
7726 score
7727 .parts
7728 .iter()
7729 .flat_map(|part| part.staves.iter())
7730 .all(|staff| staff.measures[2].section_break)
7731 );
7732 stack.undo(&mut score).expect("section break undoes");
7733 assert_eq!(
7734 serde_json::to_value(&score).unwrap(),
7735 serde_json::to_value(&original).unwrap()
7736 );
7737
7738 assert!(
7739 stack
7740 .execute(
7741 Command::SetSectionBreak(SetSectionBreakCmd {
7742 measure_index: 99,
7743 value: true,
7744 }),
7745 &mut score,
7746 )
7747 .is_err()
7748 );
7749 assert_eq!(
7750 serde_json::to_value(&score).unwrap(),
7751 serde_json::to_value(&original).unwrap()
7752 );
7753 }
7754
7755 #[test]
7756 fn notation_editing_session_keeps_score_valid_and_undoes_atomically() {
7757 let mut score = Score::new("Notation session", 120, 4, 4, 0, 1);
7758 score.parts[0].staves[0].measures[0].voices[0] = (0..4)
7759 .map(|_| Note::new(Pitch::new(Step::C, 4), Duration::Quarter))
7760 .collect();
7761 let original = serde_json::to_value(&score).unwrap();
7762 let mut stack = CommandStack::new(16);
7763 let edits = [
7764 (
7765 Command::AddPitch(AddPitchCmd {
7766 part_index: 0,
7767 staff_index: 0,
7768 measure_index: 0,
7769 voice: 0,
7770 note_index: 0,
7771 pitch: Pitch::new(Step::E, 4),
7772 }),
7773 "Add Pitch",
7774 "AddPitch",
7775 ),
7776 (
7777 Command::SetArpeggio(SetArpeggioCmd {
7778 part_index: 0,
7779 staff_index: 0,
7780 measure_index: 0,
7781 voice_index: 0,
7782 note_index: 0,
7783 direction: Some(true),
7784 }),
7785 "Set Arpeggio",
7786 "SetArpeggio",
7787 ),
7788 (
7789 Command::SetStem(SetStemCmd {
7790 part_index: 0,
7791 staff_index: 0,
7792 measure_index: 0,
7793 voice_index: 0,
7794 note_index: 0,
7795 stem_up: Some(true),
7796 }),
7797 "Set Stem",
7798 "SetStem",
7799 ),
7800 (
7801 Command::SetDynamic(SetDynamicCmd {
7802 part_index: 0,
7803 staff_index: 0,
7804 measure_index: 0,
7805 voice: 0,
7806 note_index: 0,
7807 dynamic: Some(Dynamic::Mf),
7808 }),
7809 "Set Dynamic",
7810 "SetDynamic",
7811 ),
7812 (
7813 Command::ToggleArticulation(ToggleArticulationCmd {
7814 part_index: 0,
7815 staff_index: 0,
7816 measure_index: 0,
7817 voice: 0,
7818 note_index: 1,
7819 articulation: Articulation::Staccato,
7820 }),
7821 "Toggle Articulation",
7822 "ToggleArticulation",
7823 ),
7824 (
7825 Command::ToggleTie(ToggleTieCmd {
7826 part_index: 0,
7827 staff_index: 0,
7828 measure_index: 0,
7829 voice: 0,
7830 note_index: 1,
7831 }),
7832 "Toggle Tie",
7833 "ToggleTie",
7834 ),
7835 (
7836 Command::AddHairpin(AddHairpinCmd {
7837 part_index: 0,
7838 staff_index: 0,
7839 measure_index: 0,
7840 voice: 0,
7841 start_note_idx: 0,
7842 end_note_idx: 3,
7843 kind: HairpinKind::Crescendo,
7844 }),
7845 "Add Hairpin",
7846 "AddHairpin",
7847 ),
7848 (
7849 Command::AddPedal(AddPedalCmd {
7850 part_index: 0,
7851 staff_index: 0,
7852 measure_index: 0,
7853 voice: 0,
7854 start_note_idx: 0,
7855 end_note_idx: 3,
7856 }),
7857 "Add Pedal",
7858 "AddPedal",
7859 ),
7860 (
7861 Command::SetBarline(SetBarlineCmd {
7862 measure_index: 0,
7863 side: "right".into(),
7864 barline: Barline::Double,
7865 }),
7866 "Set Barline",
7867 "SetBarline",
7868 ),
7869 ];
7870 for (command, label, key) in edits {
7871 stack.execute(command, &mut score).unwrap();
7872 assert_eq!(stack.undo_label().as_deref(), Some(label));
7873 assert_eq!(stack.undo_key().as_deref(), Some(key));
7874 }
7875 assert!(validate(&score).is_valid());
7876 let voice = &score.parts[0].staves[0].measures[0].voices[0];
7877 assert_eq!(voice[0].pitches.len(), 2);
7878 assert_eq!(voice[0].arpeggiate, Some(true));
7879 assert_eq!(voice[0].hairpin_start, Some(HairpinKind::Crescendo));
7880 assert!(voice[0].pedal_start);
7881 assert!(voice[1].tie_start && voice[2].tie_end);
7882 assert!(voice[3].pedal_end && voice[3].hairpin_end);
7883 assert_eq!(
7884 score.parts[0].staves[0].measures[0].barline_right,
7885 Barline::Double
7886 );
7887 let edited = serde_json::to_value(&score).unwrap();
7888 for _ in 0..9 {
7889 stack.undo(&mut score).unwrap();
7890 }
7891 assert_eq!(serde_json::to_value(&score).unwrap(), original);
7892 for _ in 0..9 {
7893 stack.redo(&mut score).unwrap();
7894 }
7895 assert_eq!(serde_json::to_value(&score).unwrap(), edited);
7896 }
7897
7898 #[test]
7899 fn guitar_annotation_commands_keep_json_and_score_semantics() {
7900 let mut score = Score::new("Guitar annotations", 120, 4, 4, 0, 1);
7901 score.parts[0].staves[0].measures[0].voices[0] = (0..4)
7902 .map(|_| Note::new(Pitch::new(Step::C, 4), Duration::Quarter))
7903 .collect();
7904 let edits = [
7905 (
7906 serde_json::json!({"type":"set_tablature_config", "part_index":0,
7907 "staff_index":0, "config":{"lines":6,"tuning_midi":[40,45,50,55,59,64],"capo":0}}),
7908 "SetTablatureConfig",
7909 ),
7910 (
7911 serde_json::json!({"type":"set_fingering", "part_index":0,"staff_index":0,
7912 "measure_index":0,"voice":0,"note_index":0,"fingering":2}),
7913 "SetFingering",
7914 ),
7915 (
7916 serde_json::json!({"type":"set_fingerings", "part_index":0,"staff_index":0,
7917 "measure_index":0,"voice":0,"note_index":0,"fingerings":[1,3]}),
7918 "SetFingerings",
7919 ),
7920 (
7921 serde_json::json!({"type":"set_string_number", "part_index":0,"staff_index":0,
7922 "measure_index":0,"voice":0,"note_index":0,"string_number":2}),
7923 "SetStringNumber",
7924 ),
7925 (
7926 serde_json::json!({"type":"set_tab_position", "part_index":0,"staff_index":0,
7927 "measure_index":0,"voice":0,"note_index":0,"position":{"string":2,"fret":1}}),
7928 "SetTabPosition",
7929 ),
7930 (
7931 serde_json::json!({"type":"set_guitar_technique", "part_index":0,"staff_index":0,
7932 "measure_index":0,"voice":0,"note_index":0,"technique":"slide"}),
7933 "SetGuitarTechnique",
7934 ),
7935 (
7936 serde_json::json!({"type":"set_technique_text", "part_index":0,"staff_index":0,
7937 "measure_index":0,"voice":0,"note_index":0,"text":"let ring"}),
7938 "SetTechniqueText",
7939 ),
7940 (
7941 serde_json::json!({"type":"set_note_head", "part_index":0,"staff_index":0,
7942 "measure_index":0,"voice":0,"note_index":0,"note_head":"Diamond"}),
7943 "SetNoteHead",
7944 ),
7945 (
7946 serde_json::json!({"type":"set_hidden", "part_index":0,"staff_index":0,
7947 "measure_index":0,"voice":0,"note_index":0,"hidden":true}),
7948 "SetHidden",
7949 ),
7950 (
7951 serde_json::json!({"type":"set_instrument_id", "part_index":0,"staff_index":0,
7952 "measure_index":0,"voice":0,"note_index":0,"instrument_id":"guitar"}),
7953 "SetInstrumentId",
7954 ),
7955 (
7956 serde_json::json!({"type":"set_note_placement", "part_index":0,"staff_index":0,
7957 "measure_index":0,"voice":0,"note_index":0,"offset_x":1.5,"offset_y":-2.0}),
7958 "SetNotePlacement",
7959 ),
7960 (
7961 serde_json::json!({"type":"set_expression_text", "measure_index":0,
7962 "text":"dolce"}),
7963 "SetExpressionText",
7964 ),
7965 ];
7966 for (value, key) in edits {
7967 let command: Command = serde_json::from_value(value).unwrap();
7968 assert_eq!(command_key(&command), key);
7969 assert!(!command_label(&command).is_empty());
7970 apply_command(&command, &mut score).unwrap();
7971 }
7972 let note = &score.parts[0].staves[0].measures[0].voices[0][0];
7973 assert_eq!(note.fingerings, vec![1, 3]);
7974 assert_eq!(
7975 note.tab_position,
7976 Some(crate::TabPosition { string: 2, fret: 1 })
7977 );
7978 assert_eq!(note.guitar_technique, Some(GuitarTechnique::Slide));
7979 assert_eq!(note.technique_text.as_deref(), Some("let ring"));
7980 assert_eq!(note.note_head, NoteHead::Diamond);
7981 assert!(note.hidden);
7982 assert_eq!(note.instrument_id.as_deref(), Some("guitar"));
7983 assert_eq!(note.offset_x, Some(1.5));
7984 assert_eq!(
7985 score.parts[0].staves[0].measures[0]
7986 .expression_text
7987 .as_deref(),
7988 Some("dolce")
7989 );
7990 assert!(validate(&score).is_valid());
7991 }
7992}