use super::change_hint::{ChangeHint, ChangeScope};
use super::duration::Duration;
use super::notation::{
Articulation, Barline, ChordSymbol, Clef, CrossStaff, Dynamic, FiguredBassFigure,
GuitarTechnique, HairpinKind, KeySignature, Lyric, NoteHead, OttavaKind, StyledText,
TablatureConfig, TimeSignature, TupletInfo,
};
use super::pitch::Pitch;
use super::score::{
Measure, NotationSpanner, Note, NoteAddr, Part, PartGroup, Score, ScoreTemplate, Staff,
respell_score, respell_score_to_key,
};
use super::validate::validate;
use crate::Error;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Command {
AddNote(AddNoteCmd),
AddPitch(AddPitchCmd),
SetDuration(SetDurationCmd),
DeleteNote(DeleteNoteCmd),
AddMeasure(AddMeasureCmd),
DeleteMeasure(DeleteMeasureCmd),
SetTempo(SetTempoCmd),
NewScore(NewScoreCmd),
AddHairpin(AddHairpinCmd),
ToggleTie(ToggleTieCmd),
SetDynamic(SetDynamicCmd),
ToggleArticulation(ToggleArticulationCmd),
SetKeySignature(SetKeySignatureCmd),
SetTimeSignature(SetTimeSignatureCmd),
SetBarline(SetBarlineCmd),
AddPart(AddPartCmd),
DeletePart(DeletePartCmd),
SetMetadata(SetMetadataCmd),
SetRehearsalMark(SetRehearsalMarkCmd),
SetNavigationMark(SetNavigationMarkCmd),
SetChordSymbol(SetChordSymbolCmd),
SetHarmonyRange(SetHarmonyRangeCmd),
SetFiguredBass(SetFiguredBassCmd),
SetGrace(SetGraceCmd),
SetOttava(SetOttavaCmd),
SetLyric(SetLyricCmd),
SetMultiRest(SetMultiRestCmd),
AddPedal(AddPedalCmd),
SetVolta(SetVoltaCmd),
SetClef(SetClefCmd),
SetPartName(SetPartNameCmd),
SetMidiInstrument(SetMidiInstrumentCmd),
SetTranspose(SetTransposeCmd),
SetTempoAtMeasure(SetTempoAtMeasureCmd),
PasteVoice(PasteVoiceCmd),
PasteRange(PasteRangeCmd),
SetSystemBreak(SetSystemBreakCmd),
SetPageBreak(SetPageBreakCmd),
ToggleSlur(ToggleSlurCmd),
AddStaff(AddStaffCmd),
DeleteStaff(DeleteStaffCmd),
SetTuplet(SetTupletCmd),
RespellScore(RespellScoreCmd),
RespellScoreToKey(RespellScoreToKeyCmd),
SetStem(SetStemCmd),
SetArpeggio(SetArpeggioCmd),
SetTechniqueText(SetTechniqueTextCmd),
SetFingering(SetFingeringCmd),
SetFingerings(SetFingeringsCmd),
SetStringNumber(SetStringNumberCmd),
SetTabPosition(SetTabPositionCmd),
SetTablatureConfig(SetTablatureConfigCmd),
SetNoteHead(SetNoteHeadCmd),
SetCue(SetCueCmd),
SetUnpitched(SetUnpitchedCmd),
SetInstrumentId(SetInstrumentIdCmd),
SetNotePlacement(SetNotePlacementCmd),
SetGuitarTechnique(SetGuitarTechniqueCmd),
SetGuitarBendAlter(SetGuitarBendAlterCmd),
SetExpressionText(SetExpressionTextCmd),
SetMeasureText(SetMeasureTextCmd),
SetScoreText(SetScoreTextCmd),
ToggleTrillLine(ToggleTrillLineCmd),
SetGlissando(SetGlissandoCmd),
SetCrossStaff(SetCrossStaffCmd),
SetPartGroup(SetPartGroupCmd),
AddSpanner(AddSpannerCmd),
UpdateSpanner(UpdateSpannerCmd),
RemoveSpanner(RemoveSpannerCmd),
Batch(BatchCmd),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddNoteCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub position: usize,
pub pitch: Option<Pitch>,
pub duration: Duration,
pub dot_count: u8,
pub is_rest: bool,
#[serde(default)]
pub tuplet: Option<TupletInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddPitchCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub pitch: Pitch,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetDurationCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub duration: Duration,
#[serde(default)]
pub dot_count: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddSpannerCmd {
pub spanner: NotationSpanner,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateSpannerCmd {
pub spanner: NotationSpanner,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoveSpannerCmd {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteNoteCmd {
pub note_id: String,
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddMeasureCmd {
pub after_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteMeasureCmd {
pub measure_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetTempoCmd {
pub bpm: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewScoreCmd {
pub title: String,
pub composer: String,
pub tempo_bpm: u16,
pub time_numerator: u8,
pub time_denominator: u8,
pub key_fifths: i8,
pub measure_count: u32,
#[serde(default)]
pub template: Option<ScoreTemplate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddHairpinCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub start_note_idx: usize,
pub end_note_idx: usize,
pub kind: HairpinKind,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToggleTieCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetDynamicCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub dynamic: Option<Dynamic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToggleArticulationCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub articulation: Articulation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetKeySignatureCmd {
pub fifths: i8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetTimeSignatureCmd {
pub numerator: u8,
pub denominator: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetBarlineCmd {
pub measure_index: usize,
pub side: String,
pub barline: Barline,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddPartCmd {
pub name: String,
pub short_name: String,
pub clefs: Vec<String>,
#[serde(default)]
pub midi_channel: u8,
#[serde(default)]
pub midi_program: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeletePartCmd {
pub part_index: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SetMetadataCmd {
pub title: Option<String>,
pub composer: Option<String>,
pub lyricist: Option<String>,
pub copyright: Option<String>,
pub work_number: Option<String>,
pub movement_title: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetRehearsalMarkCmd {
pub measure_index: usize,
pub text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetNavigationMarkCmd {
pub measure_index: usize,
pub mark: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetChordSymbolCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub chord: Option<ChordSymbol>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetHarmonyRangeCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub end: Option<NoteAddr>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetFiguredBassCmd {
pub measure_index: usize,
pub figures: Vec<FiguredBassFigure>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetGraceCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub is_grace: bool,
pub slash: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetOttavaCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub ottava_start: Option<OttavaKind>,
pub ottava_end: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetLyricCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub lyric: Option<Lyric>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetMultiRestCmd {
pub measure_index: usize,
pub count: Option<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetVoltaCmd {
pub measure_index: usize,
pub volta: Option<super::score::VoltaBracket>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetClefCmd {
pub part_index: usize,
pub staff_index: usize,
pub clef: Clef,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetPartNameCmd {
pub part_index: usize,
pub name: String,
pub short_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetMidiInstrumentCmd {
pub part_index: usize,
pub midi_channel: u8,
pub midi_program: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetTransposeCmd {
pub part_index: usize,
pub staff_index: usize,
pub semitones: i8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetTempoAtMeasureCmd {
pub measure_index: usize,
pub bpm: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PasteVoiceCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice_index: usize,
pub notes: Vec<Note>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetSystemBreakCmd {
pub measure_index: usize,
pub value: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetPageBreakCmd {
pub measure_index: usize,
pub value: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchCmd {
pub commands: Vec<Command>,
#[serde(default)]
pub label: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddPedalCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub start_note_idx: usize,
pub end_note_idx: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PasteRangeCmd {
pub part_index: usize,
pub staff_index: usize,
pub voice_index: usize,
pub target_measure: usize,
pub measures: Vec<Vec<Note>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToggleSlurCmd {
pub start: NoteAddr,
pub end: NoteAddr,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetPartGroupCmd {
pub group: Option<PartGroup>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToggleTrillLineCmd {
pub start: NoteAddr,
pub end: NoteAddr,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddStaffCmd {
pub part_index: usize,
pub clef: Clef,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteStaffCmd {
pub part_index: usize,
pub staff_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetTupletCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice_index: usize,
pub note_index: usize,
pub tuplet: Option<TupletInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetStemCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice_index: usize,
pub note_index: usize,
pub stem_up: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetArpeggioCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice_index: usize,
pub note_index: usize,
pub direction: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetTechniqueTextCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetGlissandoCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub start: bool,
pub end: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetCrossStaffCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub placement: Option<CrossStaff>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetFingeringCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub fingering: Option<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetFingeringsCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub fingerings: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetStringNumberCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub string_number: Option<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetTabPositionCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub position: Option<super::notation::TabPosition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetTablatureConfigCmd {
pub part_index: usize,
pub staff_index: usize,
pub config: Option<TablatureConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetGuitarTechniqueCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub technique: Option<GuitarTechnique>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetGuitarBendAlterCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub alter_cents: Option<i16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetExpressionTextCmd {
pub measure_index: usize,
pub text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetMeasureTextCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub text_index: usize,
pub text: Option<StyledText>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetScoreTextCmd {
pub text_index: usize,
pub text: Option<StyledText>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetCueCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub is_cue: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetUnpitchedCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub is_unpitched: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetInstrumentIdCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub instrument_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetNotePlacementCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
#[serde(default)]
pub offset_x: Option<f64>,
#[serde(default)]
pub offset_y: Option<f64>,
#[serde(default)]
pub relative_x: Option<f64>,
#[serde(default)]
pub relative_y: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetNoteHeadCmd {
pub part_index: usize,
pub staff_index: usize,
pub measure_index: usize,
pub voice: usize,
pub note_index: usize,
pub note_head: NoteHead,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RespellScoreCmd {
pub prefer_flat: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RespellScoreToKeyCmd {}
struct UndoEntry {
command: Command,
snapshot: Score,
}
pub struct CommandStack {
history: Vec<UndoEntry>,
future: Vec<(Command, Score)>,
max_depth: usize,
}
impl CommandStack {
pub fn new(max_depth: usize) -> Self {
Self {
history: Vec::new(),
future: Vec::new(),
max_depth,
}
}
pub fn can_undo(&self) -> bool {
!self.history.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.future.is_empty()
}
pub fn execute(&mut self, cmd: Command, score: &mut Score) -> Result<(), Error> {
let snapshot = score.clone();
let mut candidate = snapshot.clone();
apply_command(&cmd, &mut candidate)?;
if !validate(&candidate).is_valid() {
return Err(Error::InvalidScore);
}
*score = candidate;
self.history.push(UndoEntry {
command: cmd,
snapshot,
});
self.future.clear();
if self.history.len() > self.max_depth {
self.history.remove(0);
}
Ok(())
}
pub fn undo(&mut self, score: &mut Score) -> Result<ChangeHint, Error> {
let entry = self.history.last().ok_or(Error::NothingToUndo)?;
if !validate(&entry.snapshot).is_valid() {
return Err(Error::InvalidScore);
}
let entry = self.history.pop().ok_or(Error::NothingToUndo)?;
let hint = command_hint(&entry.command);
let post_snapshot = score.clone();
*score = entry.snapshot;
self.future.push((entry.command, post_snapshot));
if self.future.len() > self.max_depth {
self.future.remove(0);
}
Ok(hint)
}
pub fn redo(&mut self, score: &mut Score) -> Result<ChangeHint, Error> {
let (_, post) = self.future.last().ok_or(Error::NothingToRedo)?;
if !validate(post).is_valid() {
return Err(Error::InvalidScore);
}
let (cmd, post) = self.future.pop().ok_or(Error::NothingToRedo)?;
let hint = command_hint(&cmd);
let snapshot = score.clone();
*score = post;
self.history.push(UndoEntry {
command: cmd,
snapshot,
});
Ok(hint)
}
pub fn history_commands(&self) -> Vec<Command> {
self.history.iter().map(|e| e.command.clone()).collect()
}
pub fn undo_label(&self) -> Option<String> {
self.history.last().map(|e| command_label(&e.command))
}
pub fn redo_label(&self) -> Option<String> {
self.future.last().map(|(cmd, _)| command_label(cmd))
}
pub fn undo_key(&self) -> Option<String> {
self.history.last().map(|e| command_key(&e.command))
}
pub fn redo_key(&self) -> Option<String> {
self.future.last().map(|(cmd, _)| command_key(cmd))
}
pub fn batch_execute(&mut self, cmds: Vec<Command>, score: &mut Score) -> Result<(), Error> {
if cmds.is_empty() {
return Ok(());
}
let snapshot = score.clone();
let mut candidate = snapshot.clone();
for cmd in &cmds {
apply_command(cmd, &mut candidate)?;
}
if !validate(&candidate).is_valid() {
return Err(Error::InvalidScore);
}
*score = candidate;
self.history.push(UndoEntry {
command: Command::Batch(BatchCmd {
commands: cmds,
label: None,
}),
snapshot,
});
self.future.clear();
if self.history.len() > self.max_depth {
self.history.remove(0);
}
Ok(())
}
pub fn batch_execute_labeled(
&mut self,
cmds: Vec<Command>,
label: String,
score: &mut Score,
) -> Result<(), Error> {
if cmds.is_empty() {
return Ok(());
}
let snapshot = score.clone();
let mut candidate = snapshot.clone();
for cmd in &cmds {
apply_command(cmd, &mut candidate)?;
}
if !validate(&candidate).is_valid() {
return Err(Error::InvalidScore);
}
*score = candidate;
self.history.push(UndoEntry {
command: Command::Batch(BatchCmd {
commands: cmds,
label: Some(label),
}),
snapshot,
});
self.future.clear();
if self.history.len() > self.max_depth {
self.history.remove(0);
}
Ok(())
}
}
pub fn command_hint(cmd: &Command) -> ChangeHint {
use ChangeScope::*;
macro_rules! hint {
($scope:expr, $layout:expr, $playback:expr) => {
ChangeHint {
scope: $scope,
layout_dirty: $layout,
playback_dirty: $playback,
}
};
}
macro_rules! meas {
($c:expr) => {
Measures {
part: $c.part_index,
staff: $c.staff_index,
start: $c.measure_index,
end: $c.measure_index + 1,
}
};
}
match cmd {
Command::NewScore(_)
| Command::AddPart(_)
| Command::DeletePart(_)
| Command::AddMeasure(_)
| Command::DeleteMeasure(_) => hint!(Global, true, true),
Command::SetTempo(_) => hint!(Global, false, true),
Command::SetMetadata(_) => hint!(Global, false, false),
Command::SetKeySignature(_) => hint!(Global, true, false),
Command::SetTimeSignature(_) => hint!(Global, true, true),
Command::SetBarline(_)
| Command::SetVolta(_)
| Command::SetRehearsalMark(_)
| Command::SetNavigationMark(_)
| Command::SetExpressionText(_) => hint!(Global, false, false),
Command::SetMeasureText(c) => hint!(
Measures {
part: c.part_index,
staff: c.staff_index,
start: c.measure_index,
end: c.measure_index + 1
},
false,
false
),
Command::SetScoreText(_) => hint!(Global, false, false),
Command::SetMultiRest(_) => hint!(Global, true, false),
Command::SetTempoAtMeasure(_) => hint!(Global, false, true),
Command::SetPartName(c) => hint!(Part(c.part_index), false, false),
Command::SetMidiInstrument(c) => hint!(Part(c.part_index), false, true),
Command::SetTranspose(c) => hint!(Part(c.part_index), false, true),
Command::SetClef(c) => hint!(Part(c.part_index), true, false),
Command::AddNote(c) => hint!(meas!(c), false, true),
Command::AddPitch(c) => hint!(meas!(c), false, true),
Command::SetDuration(c) => hint!(meas!(c), false, true),
Command::DeleteNote(c) => hint!(meas!(c), false, true),
Command::PasteVoice(c) => hint!(meas!(c), false, true),
Command::PasteRange(c) => hint!(
Measures {
part: c.part_index,
staff: c.staff_index,
start: c.target_measure,
end: c.target_measure + c.measures.len()
},
false,
true
),
Command::AddHairpin(c) => hint!(meas!(c), false, true),
Command::ToggleTie(c) => hint!(meas!(c), false, true),
Command::SetDynamic(c) => hint!(meas!(c), false, true),
Command::ToggleArticulation(c) => hint!(meas!(c), false, true),
Command::SetGrace(c) => hint!(meas!(c), false, true),
Command::SetOttava(c) => hint!(meas!(c), false, true),
Command::SetLyric(c) => hint!(meas!(c), false, true),
Command::AddPedal(c) => hint!(meas!(c), false, true),
Command::SetChordSymbol(c) => hint!(meas!(c), false, true),
Command::SetHarmonyRange(c) => hint!(meas!(c), false, true),
Command::SetFiguredBass(_) => hint!(Global, false, true),
Command::SetSystemBreak(_) | Command::SetPageBreak(_) => hint!(Global, true, false),
Command::ToggleSlur(_) | Command::ToggleTrillLine(_) => hint!(Global, true, false),
Command::SetGlissando(c) => hint!(meas!(c), true, true),
Command::SetCrossStaff(c) => hint!(meas!(c), true, true),
Command::AddSpanner(_) | Command::UpdateSpanner(_) | Command::RemoveSpanner(_) => {
hint!(Global, true, true)
}
Command::SetPartGroup(_) => hint!(Global, false, false),
Command::AddStaff(_) | Command::DeleteStaff(_) => hint!(Global, true, true),
Command::SetTuplet(c) => hint!(meas!(c), false, true),
Command::RespellScore(_) | Command::RespellScoreToKey(_) => hint!(Global, true, true),
Command::SetStem(c) => hint!(meas!(c), false, false),
Command::SetArpeggio(c) => hint!(meas!(c), false, false),
Command::SetTechniqueText(c) => hint!(meas!(c), false, false),
Command::SetFingering(c) => hint!(meas!(c), false, false),
Command::SetFingerings(c) => hint!(meas!(c), false, false),
Command::SetStringNumber(c) => hint!(meas!(c), false, false),
Command::SetTabPosition(c) => hint!(meas!(c), false, false),
Command::SetTablatureConfig(c) => hint!(Part(c.part_index), true, true),
Command::SetGuitarTechnique(c) => hint!(meas!(c), false, false),
Command::SetGuitarBendAlter(c) => hint!(meas!(c), false, false),
Command::SetNoteHead(c) => hint!(meas!(c), false, false),
Command::SetCue(c) => hint!(meas!(c), false, true),
Command::SetUnpitched(c) => hint!(meas!(c), false, true),
Command::SetInstrumentId(c) => hint!(meas!(c), false, true),
Command::SetNotePlacement(c) => hint!(meas!(c), true, false),
Command::Batch(c) => {
let Some(first) = c.commands.first() else {
return hint!(Global, false, false);
};
let mut merged = command_hint(first);
for cmd in c.commands.iter().skip(1) {
merged = merged.merge(command_hint(cmd));
}
merged
}
}
}
pub fn command_label(cmd: &Command) -> String {
match cmd {
Command::AddNote(_) => "Add Note".to_string(),
Command::AddPitch(_) => "Add Pitch".to_string(),
Command::SetDuration(_) => "Set Duration".to_string(),
Command::DeleteNote(_) => "Delete Note".to_string(),
Command::AddMeasure(_) => "Add Measure".to_string(),
Command::DeleteMeasure(_) => "Delete Measure".to_string(),
Command::SetTempo(_) => "Set Tempo".to_string(),
Command::NewScore(_) => "New Score".to_string(),
Command::AddHairpin(_) => "Add Hairpin".to_string(),
Command::ToggleTie(_) => "Toggle Tie".to_string(),
Command::SetDynamic(_) => "Set Dynamic".to_string(),
Command::ToggleArticulation(_) => "Toggle Articulation".to_string(),
Command::SetKeySignature(_) => "Set Key Signature".to_string(),
Command::SetTimeSignature(_) => "Set Time Signature".to_string(),
Command::SetBarline(_) => "Set Barline".to_string(),
Command::AddPart(_) => "Add Part".to_string(),
Command::DeletePart(_) => "Delete Part".to_string(),
Command::SetMetadata(_) => "Set Metadata".to_string(),
Command::SetRehearsalMark(_) => "Set Rehearsal Mark".to_string(),
Command::SetNavigationMark(_) => "Set Navigation Mark".to_string(),
Command::SetChordSymbol(_) => "Set Chord Symbol".to_string(),
Command::SetHarmonyRange(_) => "Set Harmony Range".to_string(),
Command::SetFiguredBass(_) => "Set Figured Bass".to_string(),
Command::SetGrace(_) => "Set Grace Note".to_string(),
Command::SetOttava(_) => "Set Ottava".to_string(),
Command::SetLyric(_) => "Set Lyric".to_string(),
Command::SetMultiRest(_) => "Set Multi-Rest".to_string(),
Command::AddPedal(_) => "Add Pedal".to_string(),
Command::SetVolta(_) => "Set Volta".to_string(),
Command::SetClef(_) => "Set Clef".to_string(),
Command::SetPartName(_) => "Set Part Name".to_string(),
Command::SetMidiInstrument(_) => "Set MIDI Instrument".to_string(),
Command::SetTranspose(_) => "Set Transpose".to_string(),
Command::SetTempoAtMeasure(_) => "Set Tempo".to_string(),
Command::PasteVoice(_) => "Paste Voice".to_string(),
Command::PasteRange(_) => "Paste Range".to_string(),
Command::SetSystemBreak(_) => "Set System Break".to_string(),
Command::SetPageBreak(_) => "Set Page Break".to_string(),
Command::ToggleSlur(_) => "Toggle Slur".to_string(),
Command::AddStaff(_) => "Add Staff".to_string(),
Command::DeleteStaff(_) => "Delete Staff".to_string(),
Command::SetTuplet(c) => if c.tuplet.is_some() {
"Set Tuplet"
} else {
"Clear Tuplet"
}
.to_string(),
Command::SetInstrumentId(_) => "Set Note Instrument".to_string(),
Command::SetNotePlacement(_) => "Set Note Placement".to_string(),
Command::SetUnpitched(c) => if c.is_unpitched {
"Set Unpitched Note"
} else {
"Clear Unpitched Note"
}
.to_string(),
Command::RespellScore(c) => if c.prefer_flat {
"Respell Score (flat)"
} else {
"Respell Score (sharp)"
}
.to_string(),
Command::RespellScoreToKey(_) => "Respell Score to Key".to_string(),
Command::SetStem(_) => "Set Stem".to_string(),
Command::SetArpeggio(_) => "Set Arpeggio".to_string(),
Command::SetTechniqueText(_) => "Set Technique Text".to_string(),
Command::SetFingering(_) => "Set Fingering".to_string(),
Command::SetFingerings(_) => "Set Fingering Candidates".to_string(),
Command::SetStringNumber(_) => "Set String Number".to_string(),
Command::SetTabPosition(_) => "Set Tablature Position".to_string(),
Command::SetTablatureConfig(_) => "Set Tablature Configuration".to_string(),
Command::SetGuitarTechnique(_) => "Set Guitar Technique".to_string(),
Command::SetGuitarBendAlter(_) => "Set Guitar Bend Alter".to_string(),
Command::SetNoteHead(_) => "Set Note Head".to_string(),
Command::SetCue(c) => if c.is_cue {
"Set Cue Note"
} else {
"Clear Cue Note"
}
.to_string(),
Command::SetExpressionText(_) => "Set Expression Text".to_string(),
Command::SetMeasureText(c) => match c.text {
Some(_) => "Set Measure Text",
None => "Remove Measure Text",
}
.to_string(),
Command::SetScoreText(c) => match c.text {
Some(_) => "Set Score Text",
None => "Remove Score Text",
}
.to_string(),
Command::ToggleTrillLine(_) => "Toggle Trill Line".to_string(),
Command::SetGlissando(_) => "Set Glissando".to_string(),
Command::SetCrossStaff(_) => "Set Cross-Staff Placement".to_string(),
Command::SetPartGroup(_) => "Set Part Group".to_string(),
Command::AddSpanner(_) => "Add Notation Spanner".to_string(),
Command::UpdateSpanner(_) => "Update Notation Spanner".to_string(),
Command::RemoveSpanner(_) => "Remove Notation Spanner".to_string(),
Command::Batch(c) => c.label.clone().unwrap_or_else(|| {
c.commands
.first()
.map(command_label)
.unwrap_or_else(|| "Batch".to_string())
}),
}
}
pub fn command_key(cmd: &Command) -> String {
match cmd {
Command::AddNote(_) => "AddNote".to_string(),
Command::AddPitch(_) => "AddPitch".to_string(),
Command::SetDuration(_) => "SetDuration".to_string(),
Command::DeleteNote(_) => "DeleteNote".to_string(),
Command::AddMeasure(_) => "AddMeasure".to_string(),
Command::DeleteMeasure(_) => "DeleteMeasure".to_string(),
Command::SetTempo(_) => "SetTempo".to_string(),
Command::NewScore(_) => "NewScore".to_string(),
Command::AddHairpin(_) => "AddHairpin".to_string(),
Command::ToggleTie(_) => "ToggleTie".to_string(),
Command::SetDynamic(_) => "SetDynamic".to_string(),
Command::ToggleArticulation(_) => "ToggleArticulation".to_string(),
Command::SetKeySignature(_) => "SetKeySignature".to_string(),
Command::SetTimeSignature(_) => "SetTimeSignature".to_string(),
Command::SetBarline(_) => "SetBarline".to_string(),
Command::AddPart(_) => "AddPart".to_string(),
Command::DeletePart(_) => "DeletePart".to_string(),
Command::SetMetadata(_) => "SetMetadata".to_string(),
Command::SetRehearsalMark(_) => "SetRehearsalMark".to_string(),
Command::SetNavigationMark(_) => "SetNavigationMark".to_string(),
Command::SetChordSymbol(_) => "SetChordSymbol".to_string(),
Command::SetHarmonyRange(_) => "SetHarmonyRange".to_string(),
Command::SetFiguredBass(_) => "SetFiguredBass".to_string(),
Command::SetGrace(_) => "SetGrace".to_string(),
Command::SetOttava(_) => "SetOttava".to_string(),
Command::SetLyric(_) => "SetLyric".to_string(),
Command::SetMultiRest(_) => "SetMultiRest".to_string(),
Command::AddPedal(_) => "AddPedal".to_string(),
Command::SetVolta(_) => "SetVolta".to_string(),
Command::SetClef(_) => "SetClef".to_string(),
Command::SetPartName(_) => "SetPartName".to_string(),
Command::SetMidiInstrument(_) => "SetMidiInstrument".to_string(),
Command::SetTranspose(_) => "SetTranspose".to_string(),
Command::SetTempoAtMeasure(_) => "SetTempoAtMeasure".to_string(),
Command::PasteVoice(_) => "PasteVoice".to_string(),
Command::PasteRange(_) => "PasteRange".to_string(),
Command::SetSystemBreak(_) => "SetSystemBreak".to_string(),
Command::SetPageBreak(_) => "SetPageBreak".to_string(),
Command::ToggleSlur(_) => "ToggleSlur".to_string(),
Command::AddStaff(_) => "AddStaff".to_string(),
Command::DeleteStaff(_) => "DeleteStaff".to_string(),
Command::SetTuplet(_) => "SetTuplet".to_string(),
Command::RespellScore(_) => "RespellScore".to_string(),
Command::RespellScoreToKey(_) => "RespellScoreToKey".to_string(),
Command::SetStem(_) => "SetStem".to_string(),
Command::SetArpeggio(_) => "SetArpeggio".to_string(),
Command::SetTechniqueText(_) => "SetTechniqueText".to_string(),
Command::SetFingering(_) => "SetFingering".to_string(),
Command::SetFingerings(_) => "SetFingerings".to_string(),
Command::SetStringNumber(_) => "SetStringNumber".to_string(),
Command::SetTabPosition(_) => "SetTabPosition".to_string(),
Command::SetTablatureConfig(_) => "SetTablatureConfig".to_string(),
Command::SetGuitarTechnique(_) => "SetGuitarTechnique".to_string(),
Command::SetGuitarBendAlter(_) => "SetGuitarBendAlter".to_string(),
Command::SetNoteHead(_) => "SetNoteHead".to_string(),
Command::SetCue(_) => "SetCue".to_string(),
Command::SetUnpitched(_) => "SetUnpitched".to_string(),
Command::SetInstrumentId(_) => "SetInstrumentId".to_string(),
Command::SetNotePlacement(_) => "SetNotePlacement".to_string(),
Command::SetExpressionText(_) => "SetExpressionText".to_string(),
Command::SetMeasureText(_) => "SetMeasureText".to_string(),
Command::SetScoreText(_) => "SetScoreText".to_string(),
Command::ToggleTrillLine(_) => "ToggleTrillLine".to_string(),
Command::SetGlissando(_) => "SetGlissando".to_string(),
Command::SetCrossStaff(_) => "SetCrossStaff".to_string(),
Command::SetPartGroup(_) => "SetPartGroup".to_string(),
Command::AddSpanner(_) => "AddSpanner".to_string(),
Command::UpdateSpanner(_) => "UpdateSpanner".to_string(),
Command::RemoveSpanner(_) => "RemoveSpanner".to_string(),
Command::Batch(c) => c.label.clone().unwrap_or_else(|| "Batch".to_string()),
}
}
pub fn apply_command(cmd: &Command, score: &mut Score) -> Result<(), Error> {
match cmd {
Command::AddNote(c) => apply_add_note(c, score),
Command::AddPitch(c) => apply_add_pitch(c, score),
Command::SetDuration(c) => apply_set_duration(c, score),
Command::DeleteNote(c) => apply_delete_note(c, score),
Command::AddMeasure(c) => apply_add_measure(c, score),
Command::DeleteMeasure(c) => apply_delete_measure(c, score),
Command::SetTempo(c) => {
score.settings.tempo_bpm = c.bpm;
Ok(())
}
Command::NewScore(c) => {
let mut s = match c.template {
Some(kind) => Score::template(kind),
None => Score::new(
&c.title,
c.tempo_bpm,
c.time_numerator,
c.time_denominator,
c.key_fifths,
c.measure_count,
),
};
if c.template.is_some() {
s.metadata.title = c.title.clone();
s.metadata.composer = c.composer.clone();
s.settings.tempo_bpm = c.tempo_bpm;
s.settings.time_signature = TimeSignature {
numerator: c.time_numerator,
denominator: c.time_denominator,
};
s.settings.key_signature = KeySignature {
fifths: c.key_fifths,
mode: "major".to_string(),
};
for part in &mut s.parts {
for staff in &mut part.staves {
staff.measures.clear();
for i in 0..c.measure_count {
let mut m = Measure::empty(c.time_numerator, c.time_denominator);
m.number = i + 1;
staff.measures.push(m);
}
}
}
}
*score = s;
Ok(())
}
Command::AddHairpin(c) => apply_add_hairpin(c, score),
Command::ToggleTie(c) => apply_toggle_tie(c, score),
Command::SetDynamic(c) => apply_set_dynamic(c, score),
Command::ToggleArticulation(c) => apply_toggle_articulation(c, score),
Command::SetKeySignature(c) => {
score.settings.key_signature = KeySignature {
fifths: c.fifths,
mode: "major".to_string(),
};
Ok(())
}
Command::SetTimeSignature(c) => apply_set_time_signature(c, score),
Command::SetBarline(c) => apply_set_barline(c, score),
Command::AddPart(c) => apply_add_part(c, score),
Command::DeletePart(c) => apply_delete_part(c, score),
Command::SetMetadata(c) => apply_set_metadata(c, score),
Command::SetRehearsalMark(c) => {
for_each_measure_at(score, c.measure_index, |m| {
m.rehearsal = c.text.clone();
});
Ok(())
}
Command::SetNavigationMark(c) => {
for_each_measure_at(score, c.measure_index, |m| {
m.navigation = c.mark.clone();
});
Ok(())
}
Command::SetChordSymbol(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?
.chord_symbol = c.chord.clone();
Ok(())
}
Command::SetHarmonyRange(c) => {
if let Some(end) = &c.end
&& !score
.parts
.get(end.part)
.and_then(|part| part.staves.get(end.staff))
.and_then(|staff| staff.measures.get(end.measure))
.and_then(|measure| measure.voices.get(end.voice))
.and_then(|voice| voice.get(end.note))
.is_some()
{
return Err(Error::InvalidCommand(
"harmony range end does not point to an existing note".into(),
));
}
let note = get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?;
let chord = note.chord_symbol.as_mut().ok_or_else(|| {
Error::InvalidCommand("cannot set a harmony range without a chord symbol".into())
})?;
chord.range_end = c.end.clone();
Ok(())
}
Command::SetFiguredBass(c) => {
for part in &mut score.parts {
for staff in &mut part.staves {
if let Some(measure) = staff.measures.get_mut(c.measure_index) {
measure.figured_bass = c.figures.clone();
}
}
}
Ok(())
}
Command::SetGrace(c) => {
let note = get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?;
if note.is_rest {
return Err(Error::InvalidCommand(
"cannot make a rest into a grace note".into(),
));
}
note.is_grace = c.is_grace;
note.grace_slash = c.slash;
Ok(())
}
Command::SetOttava(c) => {
let note = get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?;
note.ottava_start = c.ottava_start;
note.ottava_end = c.ottava_end;
Ok(())
}
Command::SetLyric(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?
.lyric = c.lyric.clone();
Ok(())
}
Command::SetMultiRest(c) => {
for_each_measure_at(score, c.measure_index, |m| {
m.multi_rest_count = c.count;
});
Ok(())
}
Command::AddPedal(c) => apply_add_pedal(c, score),
Command::SetVolta(c) => {
for_each_measure_at(score, c.measure_index, |m| {
m.volta = c.volta.clone();
});
Ok(())
}
Command::SetClef(c) => apply_set_clef(c, score),
Command::SetPartName(c) => apply_set_part_name(c, score),
Command::SetMidiInstrument(c) => apply_set_midi_instrument(c, score),
Command::SetTranspose(c) => apply_set_transpose(c, score),
Command::SetTempoAtMeasure(c) => {
for_each_measure_at(score, c.measure_index, |m| {
m.tempo = c.bpm;
});
Ok(())
}
Command::PasteVoice(c) => apply_paste_voice(c, score),
Command::PasteRange(c) => apply_paste_range(c, score),
Command::SetSystemBreak(c) => {
for_each_measure_at(score, c.measure_index, |m| {
m.system_break = c.value;
});
Ok(())
}
Command::SetPageBreak(c) => {
for_each_measure_at(score, c.measure_index, |m| {
m.page_break = c.value;
});
Ok(())
}
Command::ToggleSlur(c) => apply_toggle_slur(c, score),
Command::AddStaff(c) => apply_add_staff(c, score),
Command::DeleteStaff(c) => apply_delete_staff(c, score),
Command::SetTuplet(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice_index,
c.note_index,
)?
.tuplet = c.tuplet.clone();
Ok(())
}
Command::RespellScore(c) => {
respell_score(score, c.prefer_flat);
Ok(())
}
Command::RespellScoreToKey(_) => {
respell_score_to_key(score);
Ok(())
}
Command::SetStem(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice_index,
c.note_index,
)?
.stem_up = c.stem_up;
Ok(())
}
Command::SetArpeggio(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice_index,
c.note_index,
)?
.arpeggiate = c.direction;
Ok(())
}
Command::SetTechniqueText(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?
.technique_text = c.text.clone();
Ok(())
}
Command::SetGlissando(c) => {
let note = get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?;
note.glissando_start = c.start;
note.glissando_end = c.end;
Ok(())
}
Command::SetCrossStaff(c) => {
let staff_count = score
.parts
.get(c.part_index)
.ok_or(Error::PartNotFound(c.part_index))?
.staves
.len();
let note = get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?;
if let Some(ref placement) = c.placement
&& placement.target_staff == c.staff_index
{
return Err(Error::InvalidCommand(
"cross-staff target must differ from source staff".into(),
));
}
if let Some(ref placement) = c.placement
&& placement.target_staff >= staff_count
{
return Err(Error::StaffNotFound(placement.target_staff));
}
note.cross_staff = c.placement.clone();
Ok(())
}
Command::SetFingering(c) => {
let note = get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?;
note.fingering = c.fingering;
note.fingerings = c.fingering.into_iter().collect();
Ok(())
}
Command::SetFingerings(c) => {
let note = get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?;
note.fingerings = c.fingerings.clone();
note.fingering = note.fingerings.first().copied();
Ok(())
}
Command::SetStringNumber(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?
.string_number = c.string_number;
Ok(())
}
Command::SetTabPosition(c) => {
let note = get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?;
note.string_number = c.position.as_ref().map(|position| position.string);
note.tab_position = c.position.clone();
note.tab_positions = c.position.iter().cloned().collect();
Ok(())
}
Command::SetGuitarTechnique(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?
.guitar_technique = c.technique.clone();
Ok(())
}
Command::SetGuitarBendAlter(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?
.guitar_bend_alter_cents = c.alter_cents;
Ok(())
}
Command::SetNoteHead(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?
.note_head = c.note_head.clone();
Ok(())
}
Command::SetCue(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?
.is_cue = c.is_cue;
Ok(())
}
Command::SetUnpitched(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?
.is_unpitched = c.is_unpitched;
Ok(())
}
Command::SetInstrumentId(c) => {
get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?
.instrument_id = c.instrument_id.clone();
Ok(())
}
Command::SetNotePlacement(c) => {
let note = get_note_mut(
score,
c.part_index,
c.staff_index,
c.measure_index,
c.voice,
c.note_index,
)?;
for value in [c.offset_x, c.offset_y, c.relative_x, c.relative_y] {
if value.is_some_and(|value| !value.is_finite()) {
return Err(Error::InvalidCommand(
"note placement offsets must be finite".into(),
));
}
}
note.offset_x = c.offset_x;
note.offset_y = c.offset_y;
note.relative_x = c.relative_x;
note.relative_y = c.relative_y;
Ok(())
}
Command::SetExpressionText(c) => {
for_each_measure_at(score, c.measure_index, |m| {
m.expression_text = c.text.clone();
});
Ok(())
}
Command::SetMeasureText(c) => apply_set_measure_text(c, score),
Command::SetScoreText(c) => apply_set_score_text(c, score),
Command::SetTablatureConfig(c) => apply_set_tablature_config(c, score),
Command::ToggleTrillLine(c) => apply_toggle_trill_line(c, score),
Command::SetPartGroup(c) => {
if let Some(group) = &c.group {
score
.part_groups
.retain(|g| g.first_part != group.first_part || g.last_part != group.last_part);
score.part_groups.push(group.clone());
} else {
score.part_groups.clear();
}
Ok(())
}
Command::AddSpanner(c) => {
if score
.spanners
.iter()
.any(|spanner| spanner.id == c.spanner.id)
{
return Err(Error::InvalidCommand(format!(
"notation spanner id already exists: {}",
c.spanner.id
)));
}
score.spanners.push(c.spanner.clone());
Ok(())
}
Command::UpdateSpanner(c) => {
let existing = score
.spanners
.iter_mut()
.find(|spanner| spanner.id == c.spanner.id)
.ok_or_else(|| {
Error::InvalidCommand(format!(
"notation spanner id does not exist: {}",
c.spanner.id
))
})?;
*existing = c.spanner.clone();
Ok(())
}
Command::RemoveSpanner(c) => {
let index = score
.spanners
.iter()
.position(|spanner| spanner.id == c.id)
.ok_or_else(|| {
Error::InvalidCommand(format!("notation spanner id does not exist: {}", c.id))
})?;
score.spanners.remove(index);
Ok(())
}
Command::Batch(c) => {
for cmd in &c.commands {
apply_command(cmd, score)?;
}
Ok(())
}
}
}
fn apply_set_tablature_config(cmd: &SetTablatureConfigCmd, score: &mut Score) -> Result<(), Error> {
let staff = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or_else(|| Error::InvalidCommand(format!("staff {} out of range", cmd.staff_index)))?;
staff.tablature = cmd.config.clone();
Ok(())
}
fn get_note_mut(
score: &mut Score,
part_index: usize,
staff_index: usize,
measure_index: usize,
voice: usize,
note_index: usize,
) -> Result<&mut Note, Error> {
score
.parts
.get_mut(part_index)
.ok_or(Error::PartNotFound(part_index))?
.staves
.get_mut(staff_index)
.ok_or(Error::StaffNotFound(staff_index))?
.measures
.get_mut(measure_index)
.ok_or(Error::MeasureNotFound(measure_index))?
.voices
.get_mut(voice)
.ok_or(Error::VoiceOutOfRange(voice))?
.get_mut(note_index)
.ok_or(Error::NoteNotFound(note_index))
}
fn for_each_measure_at(score: &mut Score, index: usize, mut f: impl FnMut(&mut Measure)) {
for part in &mut score.parts {
for staff in &mut part.staves {
if let Some(m) = staff.measures.get_mut(index) {
f(m);
}
}
}
}
fn apply_set_measure_text(cmd: &SetMeasureTextCmd, score: &mut Score) -> Result<(), Error> {
let measure = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.measures
.get_mut(cmd.measure_index)
.ok_or(Error::MeasureNotFound(cmd.measure_index))?;
if cmd.text_index > measure.texts.len()
|| (cmd.text.is_none() && cmd.text_index == measure.texts.len())
{
return Err(Error::InvalidCommand(format!(
"styled text index {} out of range for {} entries",
cmd.text_index,
measure.texts.len()
)));
}
if let Some(text) = &cmd.text {
if cmd.text_index == measure.texts.len() {
measure.texts.push(text.clone());
} else {
measure.texts[cmd.text_index] = text.clone();
}
} else {
measure.texts.remove(cmd.text_index);
}
Ok(())
}
fn apply_set_score_text(cmd: &SetScoreTextCmd, score: &mut Score) -> Result<(), Error> {
if cmd.text_index > score.texts.len()
|| (cmd.text.is_none() && cmd.text_index == score.texts.len())
{
return Err(Error::InvalidCommand(format!(
"styled score text index {} out of range for {} entries",
cmd.text_index,
score.texts.len()
)));
}
if let Some(text) = &cmd.text {
if cmd.text_index == score.texts.len() {
score.texts.push(text.clone());
} else {
score.texts[cmd.text_index] = text.clone();
}
} else {
score.texts.remove(cmd.text_index);
}
Ok(())
}
fn apply_add_note(cmd: &AddNoteCmd, score: &mut Score) -> Result<(), Error> {
let ts_beats = score.settings.time_signature.total_beats();
let note = if cmd.is_rest {
let mut n = Note::rest(cmd.duration.clone());
n.dot_count = cmd.dot_count;
n.tuplet = cmd.tuplet.clone();
n
} else {
let pitch = cmd
.pitch
.clone()
.ok_or_else(|| Error::InvalidCommand("pitch required for non-rest note".into()))?;
let mut n = Note::new(pitch, cmd.duration.clone());
n.dot_count = cmd.dot_count;
n.tuplet = cmd.tuplet.clone();
n
};
let pos = {
let voice = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.measures
.get_mut(cmd.measure_index)
.ok_or(Error::MeasureNotFound(cmd.measure_index))?
.voices
.get_mut(cmd.voice)
.ok_or(Error::VoiceOutOfRange(cmd.voice))?;
let pos = cmd.position.min(voice.len());
voice.insert(pos, note);
trim_voice_to_measure(voice, ts_beats);
pos
};
remap_spanners(score, |address| {
if same_voice(
address,
cmd.part_index,
cmd.staff_index,
cmd.measure_index,
cmd.voice,
) && address.note >= pos
{
let mut shifted = address.clone();
shifted.note += 1;
Some(shifted)
} else {
Some(address.clone())
}
});
Ok(())
}
fn apply_add_pitch(cmd: &AddPitchCmd, score: &mut Score) -> Result<(), Error> {
let voice = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.measures
.get_mut(cmd.measure_index)
.ok_or(Error::MeasureNotFound(cmd.measure_index))?
.voices
.get_mut(cmd.voice)
.ok_or(Error::VoiceOutOfRange(cmd.voice))?;
let note = voice
.get_mut(cmd.note_index)
.ok_or(Error::NoteNotFound(cmd.note_index))?;
if note.is_rest {
return Err(Error::InvalidCommand("cannot add pitch to a rest".into()));
}
if !note
.pitches
.iter()
.any(|p| p.step == cmd.pitch.step && p.octave == cmd.pitch.octave)
{
note.pitches.push(cmd.pitch.clone());
}
Ok(())
}
fn apply_set_duration(cmd: &SetDurationCmd, score: &mut Score) -> Result<(), Error> {
let ts_beats = score.settings.time_signature.total_beats();
{
let voice = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.measures
.get_mut(cmd.measure_index)
.ok_or(Error::MeasureNotFound(cmd.measure_index))?
.voices
.get_mut(cmd.voice)
.ok_or(Error::VoiceOutOfRange(cmd.voice))?;
let note = voice
.get_mut(cmd.note_index)
.ok_or(Error::NoteNotFound(cmd.note_index))?;
note.duration = cmd.duration.clone();
note.dot_count = cmd.dot_count;
trim_voice_to_measure(voice, ts_beats);
}
prune_orphaned_spanners(score);
Ok(())
}
fn apply_delete_note(cmd: &DeleteNoteCmd, score: &mut Score) -> Result<(), Error> {
let ts_beats = score.settings.time_signature.total_beats();
let deleted_position = {
let voice = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.measures
.get_mut(cmd.measure_index)
.ok_or(Error::MeasureNotFound(cmd.measure_index))?
.voices
.get_mut(cmd.voice)
.ok_or(Error::VoiceOutOfRange(cmd.voice))?;
let position = voice.iter().position(|note| note.id == cmd.note_id);
voice.retain(|note| note.id != cmd.note_id);
pad_voice_to_measure(voice, ts_beats);
position
};
if let Some(position) = deleted_position {
remap_spanners(score, |address| {
if !same_voice(
address,
cmd.part_index,
cmd.staff_index,
cmd.measure_index,
cmd.voice,
) {
return Some(address.clone());
}
if address.note == position {
None
} else if address.note > position {
let mut shifted = address.clone();
shifted.note -= 1;
Some(shifted)
} else {
Some(address.clone())
}
});
} else {
prune_orphaned_spanners(score);
}
Ok(())
}
fn apply_add_measure(cmd: &AddMeasureCmd, score: &mut Score) -> Result<(), Error> {
let ts = score.settings.time_signature.clone();
let insert_at = cmd.after_index.saturating_add(1);
for part in &mut score.parts {
for staff in &mut part.staves {
let insert_at = insert_at.min(staff.measures.len());
let mut m = Measure::empty(ts.numerator, ts.denominator);
m.number = insert_at as u32 + 1;
staff.measures.insert(insert_at, m);
for (i, measure) in staff.measures.iter_mut().enumerate() {
measure.number = i as u32 + 1;
}
}
}
remap_spanners(score, |address| {
if address.measure >= insert_at {
let mut shifted = address.clone();
shifted.measure += 1;
Some(shifted)
} else {
Some(address.clone())
}
});
Ok(())
}
fn apply_delete_measure(cmd: &DeleteMeasureCmd, score: &mut Score) -> Result<(), Error> {
for part in &mut score.parts {
for staff in &mut part.staves {
if cmd.measure_index < staff.measures.len() {
staff.measures.remove(cmd.measure_index);
for (i, m) in staff.measures.iter_mut().enumerate() {
m.number = i as u32 + 1;
}
}
}
}
remap_spanners(score, |address| {
if address.measure == cmd.measure_index {
None
} else if address.measure > cmd.measure_index {
let mut shifted = address.clone();
shifted.measure -= 1;
Some(shifted)
} else {
Some(address.clone())
}
});
Ok(())
}
fn apply_add_hairpin(cmd: &AddHairpinCmd, score: &mut Score) -> Result<(), Error> {
let voice = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.measures
.get_mut(cmd.measure_index)
.ok_or(Error::MeasureNotFound(cmd.measure_index))?
.voices
.get_mut(cmd.voice)
.ok_or(Error::VoiceOutOfRange(cmd.voice))?;
if cmd.start_note_idx >= voice.len() {
return Err(Error::NoteNotFound(cmd.start_note_idx));
}
if cmd.end_note_idx >= voice.len() {
return Err(Error::NoteNotFound(cmd.end_note_idx));
}
if cmd.start_note_idx >= cmd.end_note_idx {
return Err(Error::InvalidCommand(
"start_note_idx must be less than end_note_idx".into(),
));
}
for note in voice
.iter_mut()
.take(cmd.end_note_idx + 1)
.skip(cmd.start_note_idx)
{
note.hairpin_start = None;
note.hairpin_end = false;
}
voice[cmd.start_note_idx].hairpin_start = Some(cmd.kind);
voice[cmd.end_note_idx].hairpin_end = true;
Ok(())
}
fn apply_add_pedal(cmd: &AddPedalCmd, score: &mut Score) -> Result<(), Error> {
let voice = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.measures
.get_mut(cmd.measure_index)
.ok_or(Error::MeasureNotFound(cmd.measure_index))?
.voices
.get_mut(cmd.voice)
.ok_or(Error::VoiceOutOfRange(cmd.voice))?;
if cmd.start_note_idx >= voice.len() {
return Err(Error::NoteNotFound(cmd.start_note_idx));
}
if cmd.end_note_idx >= voice.len() {
return Err(Error::NoteNotFound(cmd.end_note_idx));
}
if cmd.start_note_idx >= cmd.end_note_idx {
return Err(Error::InvalidCommand(
"start_note_idx must be less than end_note_idx".into(),
));
}
for note in voice
.iter_mut()
.take(cmd.end_note_idx + 1)
.skip(cmd.start_note_idx)
{
note.pedal_start = false;
note.pedal_end = false;
}
voice[cmd.start_note_idx].pedal_start = true;
voice[cmd.end_note_idx].pedal_end = true;
Ok(())
}
fn apply_toggle_tie(cmd: &ToggleTieCmd, score: &mut Score) -> Result<(), Error> {
let current_tie_start = {
let v = score
.parts
.get(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.measures
.get(cmd.measure_index)
.ok_or(Error::MeasureNotFound(cmd.measure_index))?
.voices
.get(cmd.voice)
.ok_or(Error::VoiceOutOfRange(cmd.voice))?;
v.get(cmd.note_index)
.ok_or(Error::NoteNotFound(cmd.note_index))?
.tie_start
};
let voice_len = score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index]
.voices[cmd.voice]
.len();
let total_measures = score.parts[cmd.part_index].staves[cmd.staff_index]
.measures
.len();
let new_tie = !current_tie_start;
score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index].voices
[cmd.voice][cmd.note_index]
.tie_start = new_tie;
if cmd.note_index + 1 < voice_len {
score.parts[cmd.part_index].staves[cmd.staff_index].measures[cmd.measure_index].voices
[cmd.voice][cmd.note_index + 1]
.tie_end = new_tie;
} else {
let next_mi = cmd.measure_index + 1;
if next_mi < total_measures {
let next_voice = &mut score.parts[cmd.part_index].staves[cmd.staff_index].measures
[next_mi]
.voices[cmd.voice];
if let Some(n) = next_voice.get_mut(0) {
n.tie_end = new_tie;
}
}
}
Ok(())
}
fn apply_set_dynamic(cmd: &SetDynamicCmd, score: &mut Score) -> Result<(), Error> {
get_note_mut(
score,
cmd.part_index,
cmd.staff_index,
cmd.measure_index,
cmd.voice,
cmd.note_index,
)?
.dynamic = cmd.dynamic.clone();
Ok(())
}
fn apply_toggle_articulation(cmd: &ToggleArticulationCmd, score: &mut Score) -> Result<(), Error> {
let note = get_note_mut(
score,
cmd.part_index,
cmd.staff_index,
cmd.measure_index,
cmd.voice,
cmd.note_index,
)?;
if let Some(pos) = note
.articulations
.iter()
.position(|a| a == &cmd.articulation)
{
note.articulations.remove(pos);
} else {
note.articulations.push(cmd.articulation.clone());
}
Ok(())
}
fn apply_set_time_signature(cmd: &SetTimeSignatureCmd, score: &mut Score) -> Result<(), Error> {
if cmd.numerator == 0 || cmd.denominator == 0 {
return Err(Error::InvalidCommand(
"time signature numerator and denominator must be > 0".into(),
));
}
if ![1u8, 2, 4, 8, 16, 32].contains(&cmd.denominator) {
return Err(Error::InvalidCommand(format!(
"invalid time signature denominator: {}",
cmd.denominator
)));
}
score.settings.time_signature = TimeSignature {
numerator: cmd.numerator,
denominator: cmd.denominator,
};
let max_beats = score.settings.time_signature.total_beats();
for part in &mut score.parts {
for staff in &mut part.staves {
for measure in &mut staff.measures {
for voice in &mut measure.voices {
trim_voice_to_measure(voice, max_beats);
pad_voice_to_measure(voice, max_beats);
}
}
}
}
Ok(())
}
fn apply_set_barline(cmd: &SetBarlineCmd, score: &mut Score) -> Result<(), Error> {
for part in &mut score.parts {
for staff in &mut part.staves {
let measure = staff
.measures
.get_mut(cmd.measure_index)
.ok_or(Error::MeasureNotFound(cmd.measure_index))?;
match cmd.side.as_str() {
"left" => measure.barline_left = cmd.barline.clone(),
"right" => measure.barline_right = cmd.barline.clone(),
_ => {
return Err(Error::InvalidCommand(format!(
"invalid barline side: '{}'",
cmd.side
)));
}
}
}
}
Ok(())
}
fn apply_add_part(cmd: &AddPartCmd, score: &mut Score) -> Result<(), Error> {
if cmd.clefs.is_empty() {
return Err(Error::InvalidCommand(
"AddPart requires at least one clef".into(),
));
}
let measure_count = score.measure_count();
let ts = score.settings.time_signature.clone();
let mut part = Part::new(&cmd.name, &cmd.short_name);
part.midi_channel = cmd.midi_channel.min(15);
part.midi_program = cmd.midi_program;
for clef_str in &cmd.clefs {
let clef = match clef_str.as_str() {
"Bass" => Clef::Bass,
"Alto" => Clef::Alto,
"Tenor" => Clef::Tenor,
"Percussion" => Clef::Percussion,
_ => Clef::Treble,
};
let mut staff = Staff::new(clef);
for i in 0..measure_count {
let mut m = Measure::empty(ts.numerator, ts.denominator);
m.number = i as u32 + 1;
staff.measures.push(m);
}
part.staves.push(staff);
}
score.parts.push(part);
Ok(())
}
fn apply_set_clef(cmd: &SetClefCmd, score: &mut Score) -> Result<(), Error> {
score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.clef = cmd.clef.clone();
Ok(())
}
fn apply_set_part_name(cmd: &SetPartNameCmd, score: &mut Score) -> Result<(), Error> {
let part = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?;
part.name = cmd.name.clone();
part.short_name = cmd.short_name.clone();
Ok(())
}
fn apply_delete_part(cmd: &DeletePartCmd, score: &mut Score) -> Result<(), Error> {
if cmd.part_index >= score.parts.len() {
return Err(Error::PartNotFound(cmd.part_index));
}
score.parts.remove(cmd.part_index);
remap_spanners(score, |address| {
if address.part == cmd.part_index {
None
} else if address.part > cmd.part_index {
let mut shifted = address.clone();
shifted.part -= 1;
Some(shifted)
} else {
Some(address.clone())
}
});
Ok(())
}
fn apply_set_metadata(cmd: &SetMetadataCmd, score: &mut Score) -> Result<(), Error> {
if let Some(v) = &cmd.title {
score.metadata.title = v.clone();
}
if let Some(v) = &cmd.composer {
score.metadata.composer = v.clone();
}
if let Some(v) = &cmd.lyricist {
score.metadata.lyricist = v.clone();
}
if let Some(v) = &cmd.copyright {
score.metadata.copyright = v.clone();
}
if let Some(v) = &cmd.work_number {
score.metadata.work_number = v.clone();
}
if let Some(v) = &cmd.movement_title {
score.metadata.movement_title = v.clone();
}
Ok(())
}
fn apply_set_midi_instrument(cmd: &SetMidiInstrumentCmd, score: &mut Score) -> Result<(), Error> {
let part = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?;
part.midi_channel = cmd.midi_channel.min(15);
part.midi_program = cmd.midi_program;
Ok(())
}
fn apply_set_transpose(cmd: &SetTransposeCmd, score: &mut Score) -> Result<(), Error> {
score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.transpose_semitones = cmd.semitones;
Ok(())
}
fn apply_paste_voice(cmd: &PasteVoiceCmd, score: &mut Score) -> Result<(), Error> {
let endpoint_ids = capture_replaced_endpoint_ids(score, |address| {
same_voice(
address,
cmd.part_index,
cmd.staff_index,
cmd.measure_index,
cmd.voice_index,
)
});
{
let voice = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?
.measures
.get_mut(cmd.measure_index)
.ok_or(Error::MeasureNotFound(cmd.measure_index))?
.voices
.get_mut(cmd.voice_index)
.ok_or(Error::VoiceOutOfRange(cmd.voice_index))?;
*voice = cmd.notes.clone();
}
remap_replaced_spanner_endpoints(score, &endpoint_ids);
Ok(())
}
fn apply_paste_range(cmd: &PasteRangeCmd, score: &mut Score) -> Result<(), Error> {
if cmd.voice_index >= 4 {
return Err(Error::VoiceOutOfRange(cmd.voice_index));
}
let endpoint_ids = capture_replaced_endpoint_ids(score, |address| {
address.part == cmd.part_index
&& address.staff == cmd.staff_index
&& address.voice == cmd.voice_index
&& address.measure >= cmd.target_measure
&& address.measure < cmd.target_measure.saturating_add(cmd.measures.len())
});
let part = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?;
let staff = part
.staves
.get_mut(cmd.staff_index)
.ok_or(Error::StaffNotFound(cmd.staff_index))?;
for (offset, notes) in cmd.measures.iter().enumerate() {
let mi = cmd.target_measure + offset;
let measure = staff
.measures
.get_mut(mi)
.ok_or(Error::MeasureNotFound(mi))?;
measure.voices[cmd.voice_index] = notes.clone();
}
remap_replaced_spanner_endpoints(score, &endpoint_ids);
Ok(())
}
fn apply_toggle_slur(cmd: &ToggleSlurCmd, score: &mut Score) -> Result<(), Error> {
let new_start = !{
score
.parts
.get(cmd.start.part)
.ok_or(Error::PartNotFound(cmd.start.part))?
.staves
.get(cmd.start.staff)
.ok_or(Error::StaffNotFound(cmd.start.staff))?
.measures
.get(cmd.start.measure)
.ok_or(Error::MeasureNotFound(cmd.start.measure))?
.voices
.get(cmd.start.voice)
.ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
.get(cmd.start.note)
.ok_or(Error::NoteNotFound(cmd.start.note))?
.slur_start
};
let new_end = !{
score
.parts
.get(cmd.end.part)
.ok_or(Error::PartNotFound(cmd.end.part))?
.staves
.get(cmd.end.staff)
.ok_or(Error::StaffNotFound(cmd.end.staff))?
.measures
.get(cmd.end.measure)
.ok_or(Error::MeasureNotFound(cmd.end.measure))?
.voices
.get(cmd.end.voice)
.ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
.get(cmd.end.note)
.ok_or(Error::NoteNotFound(cmd.end.note))?
.slur_end
};
score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
[cmd.start.voice][cmd.start.note]
.slur_start = new_start;
score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
[cmd.end.voice][cmd.end.note]
.slur_end = new_end;
Ok(())
}
fn apply_toggle_trill_line(cmd: &ToggleTrillLineCmd, score: &mut Score) -> Result<(), Error> {
let new_start = !{
score
.parts
.get(cmd.start.part)
.ok_or(Error::PartNotFound(cmd.start.part))?
.staves
.get(cmd.start.staff)
.ok_or(Error::StaffNotFound(cmd.start.staff))?
.measures
.get(cmd.start.measure)
.ok_or(Error::MeasureNotFound(cmd.start.measure))?
.voices
.get(cmd.start.voice)
.ok_or(Error::VoiceOutOfRange(cmd.start.voice))?
.get(cmd.start.note)
.ok_or(Error::NoteNotFound(cmd.start.note))?
.trill_line_start
};
let new_end = !{
score
.parts
.get(cmd.end.part)
.ok_or(Error::PartNotFound(cmd.end.part))?
.staves
.get(cmd.end.staff)
.ok_or(Error::StaffNotFound(cmd.end.staff))?
.measures
.get(cmd.end.measure)
.ok_or(Error::MeasureNotFound(cmd.end.measure))?
.voices
.get(cmd.end.voice)
.ok_or(Error::VoiceOutOfRange(cmd.end.voice))?
.get(cmd.end.note)
.ok_or(Error::NoteNotFound(cmd.end.note))?
.trill_line_end
};
score.parts[cmd.start.part].staves[cmd.start.staff].measures[cmd.start.measure].voices
[cmd.start.voice][cmd.start.note]
.trill_line_start = new_start;
score.parts[cmd.end.part].staves[cmd.end.staff].measures[cmd.end.measure].voices
[cmd.end.voice][cmd.end.note]
.trill_line_end = new_end;
Ok(())
}
fn apply_add_staff(cmd: &AddStaffCmd, score: &mut Score) -> Result<(), Error> {
let ts = score.settings.time_signature.clone();
let measure_count = score
.parts
.get(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?
.staves
.first()
.map_or(0, |s| s.measures.len());
let mut staff = Staff::new(cmd.clef.clone());
for i in 0..measure_count {
let mut m = Measure::empty(ts.numerator, ts.denominator);
m.number = i as u32 + 1;
staff.measures.push(m);
}
score.parts[cmd.part_index].staves.push(staff);
Ok(())
}
fn apply_delete_staff(cmd: &DeleteStaffCmd, score: &mut Score) -> Result<(), Error> {
let part = score
.parts
.get_mut(cmd.part_index)
.ok_or(Error::PartNotFound(cmd.part_index))?;
if part.staves.len() <= 1 {
return Err(Error::CannotDeleteLastStaff);
}
if cmd.staff_index >= part.staves.len() {
return Err(Error::StaffNotFound(cmd.staff_index));
}
part.staves.remove(cmd.staff_index);
remap_spanners(score, |address| {
if address.part != cmd.part_index {
Some(address.clone())
} else if address.staff == cmd.staff_index {
None
} else if address.staff > cmd.staff_index {
let mut shifted = address.clone();
shifted.staff -= 1;
Some(shifted)
} else {
Some(address.clone())
}
});
Ok(())
}
fn same_voice(address: &NoteAddr, part: usize, staff: usize, measure: usize, voice: usize) -> bool {
address.part == part
&& address.staff == staff
&& address.measure == measure
&& address.voice == voice
}
fn note_at<'a>(score: &'a Score, address: &NoteAddr) -> Option<&'a Note> {
score
.parts
.get(address.part)
.and_then(|part| part.staves.get(address.staff))
.and_then(|staff| staff.measures.get(address.measure))
.and_then(|measure| measure.voices.get(address.voice))
.and_then(|voice| voice.get(address.note))
}
fn remap_spanners(score: &mut Score, mut remap: impl FnMut(&NoteAddr) -> Option<NoteAddr>) {
let spanners = std::mem::take(&mut score.spanners);
score.spanners = spanners
.into_iter()
.filter_map(|mut spanner| {
spanner.start = remap(&spanner.start)?;
spanner.end = remap(&spanner.end)?;
(note_at(score, &spanner.start).is_some() && note_at(score, &spanner.end).is_some())
.then_some(spanner)
})
.collect();
}
fn prune_orphaned_spanners(score: &mut Score) {
remap_spanners(score, |address| Some(address.clone()));
}
fn capture_replaced_endpoint_ids(
score: &Score,
mut replaced: impl FnMut(&NoteAddr) -> bool,
) -> Vec<(NoteAddr, String)> {
score
.spanners
.iter()
.flat_map(|spanner| [&spanner.start, &spanner.end])
.filter(|address| replaced(address))
.filter_map(|address| {
note_at(score, address).map(|note| (address.clone(), note.id.clone()))
})
.collect()
}
fn remap_replaced_spanner_endpoints(score: &mut Score, endpoint_ids: &[(NoteAddr, String)]) {
let mapped: Vec<(NoteAddr, Option<NoteAddr>)> = endpoint_ids
.iter()
.map(|(address, note_id)| {
let matches: Vec<usize> = score
.parts
.get(address.part)
.and_then(|part| part.staves.get(address.staff))
.and_then(|staff| staff.measures.get(address.measure))
.and_then(|measure| measure.voices.get(address.voice))
.map(|voice| {
voice
.iter()
.enumerate()
.filter_map(|(index, note)| (note.id == *note_id).then_some(index))
.collect()
})
.unwrap_or_default();
let replacement = (matches.len() == 1).then(|| NoteAddr {
note: matches[0],
..address.clone()
});
(address.clone(), replacement)
})
.collect();
remap_spanners(score, |address| {
mapped
.iter()
.find(|(old, _)| old == address)
.map(|(_, replacement)| replacement.clone())
.unwrap_or_else(|| Some(address.clone()))
});
}
fn trim_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
let mut total = 0.0f64;
let mut cutoff = voice.len();
for (i, n) in voice.iter().enumerate() {
total += n.beats();
if total > max_beats + 1e-9 {
cutoff = i;
break;
}
}
voice.truncate(cutoff);
pad_voice_to_measure(voice, max_beats);
}
fn pad_voice_to_measure(voice: &mut Vec<Note>, max_beats: f64) {
let mut used: f64 = voice.iter().map(|n| n.beats()).sum();
while max_beats - used > 1e-9 {
let remaining = max_beats - used;
let rest = Note::rest(Duration::whole_filling_beats(remaining));
used += rest.beats();
voice.push(rest);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ScoreEngine;
use crate::model::pitch::Step;
fn default_engine_score() -> Score {
let mut s = Score::default();
for part in &mut s.parts {
for staff in &mut part.staves {
for (i, m) in staff.measures.iter_mut().enumerate() {
m.number = i as u32 + 1;
}
}
}
s
}
fn score_with_typed_spanner() -> Score {
use crate::model::score::{NotationSpanner, NotationSpannerKind};
let mut score = Score::new("Spanner edits", 120, 4, 4, 0, 1);
score.parts[0].staves[0].measures[0].voices[0] = (0..4)
.map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
.collect();
score.spanners.push(NotationSpanner {
id: "typed-glissando".to_string(),
kind: NotationSpannerKind::Glissando,
start: NoteAddr {
part: 0,
staff: 0,
measure: 0,
voice: 0,
note: 1,
},
end: NoteAddr {
part: 0,
staff: 0,
measure: 0,
voice: 0,
note: 2,
},
number: Some(1),
line_type: None,
text: None,
placement: None,
ottava_size: None,
ottava_type: None,
});
score
}
fn insert_note_at(position: usize) -> Command {
Command::AddNote(AddNoteCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
position,
pitch: Some(Pitch::new(Step::D, 5)),
duration: Duration::Quarter,
dot_count: 0,
is_rest: false,
tuplet: None,
})
}
#[test]
fn add_note_inserts_into_voice() {
let mut score = default_engine_score();
let cmd = Command::AddNote(AddNoteCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
position: 0,
pitch: Some(Pitch::new(Step::C, 4)),
duration: Duration::Quarter,
dot_count: 0,
is_rest: false,
tuplet: None,
});
apply_command(&cmd, &mut score).unwrap();
let first = &score.parts[0].staves[0].measures[0].voices[0][0];
assert!(!first.is_rest);
assert_eq!(first.pitches[0].step, Step::C);
}
#[test]
fn typed_spanners_follow_note_insertions_at_every_relative_position() {
for (position, expected_start, expected_end) in [
(0, 2, 3), (1, 2, 3), (2, 1, 3), (4, 1, 2), (3, 1, 2), ] {
let mut score = score_with_typed_spanner();
apply_command(&insert_note_at(position), &mut score).unwrap();
let span = score.spanners.first().expect("span remains attached");
assert_eq!(span.start.note, expected_start, "position {position}");
assert_eq!(span.end.note, expected_end, "position {position}");
}
}
#[test]
fn deleting_a_typed_spanner_endpoint_removes_the_whole_span_and_undo_redo_is_atomic() {
let mut score = score_with_typed_spanner();
let endpoint_id = score.parts[0].staves[0].measures[0].voices[0][1].id.clone();
let mut stack = CommandStack::new(8);
stack
.execute(
Command::DeleteNote(DeleteNoteCmd {
note_id: endpoint_id,
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
}),
&mut score,
)
.unwrap();
assert!(score.spanners.is_empty());
stack.undo(&mut score).unwrap();
assert_eq!(score.spanners[0].start.note, 1);
assert_eq!(score.spanners[0].end.note, 2);
stack.redo(&mut score).unwrap();
assert!(score.spanners.is_empty());
}
#[test]
fn batched_structural_edits_remap_typed_spanners_and_restore_them_on_undo() {
let mut score = score_with_typed_spanner();
let mut stack = CommandStack::new(8);
stack
.batch_execute(
vec![
insert_note_at(0),
Command::SetTempo(SetTempoCmd { bpm: 144 }),
],
&mut score,
)
.unwrap();
assert_eq!(score.spanners[0].start.note, 2);
assert_eq!(score.spanners[0].end.note, 3);
stack.undo(&mut score).unwrap();
assert_eq!(score.spanners[0].start.note, 1);
assert_eq!(score.spanners[0].end.note, 2);
stack.redo(&mut score).unwrap();
assert_eq!(score.spanners[0].start.note, 2);
assert_eq!(score.spanners[0].end.note, 3);
}
#[test]
fn typed_spanners_remap_only_the_edited_endpoint_across_staff_and_voice() {
use crate::model::score::{NotationSpanner, NotationSpannerKind, ScoreTemplate};
let mut score = Score::template(ScoreTemplate::Piano);
for staff in &mut score.parts[0].staves {
let notes: Vec<Note> = (0..4)
.map(|offset| Note::new(Pitch::new(Step::C, 4 + offset), Duration::Quarter))
.collect();
staff.measures[0].voices[0] = notes.clone();
staff.measures[0].voices[1] = notes;
}
score.spanners.push(NotationSpanner {
id: "cross-staff".to_string(),
kind: NotationSpannerKind::Slur,
start: NoteAddr {
part: 0,
staff: 0,
measure: 0,
voice: 0,
note: 1,
},
end: NoteAddr {
part: 0,
staff: 1,
measure: 0,
voice: 1,
note: 2,
},
number: Some(2),
line_type: None,
text: None,
placement: None,
ottava_size: None,
ottava_type: None,
});
let mut stack = CommandStack::new(8);
stack.execute(insert_note_at(0), &mut score).unwrap();
let span = &score.spanners[0];
assert_eq!(span.start.note, 2);
assert_eq!(span.end.staff, 1);
assert_eq!(span.end.voice, 1);
assert_eq!(span.end.note, 2);
}
#[test]
fn set_fingerings_keeps_first_legacy_value_in_sync() {
let mut score = default_engine_score();
apply_command(
&Command::AddNote(AddNoteCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
position: 0,
pitch: Some(Pitch::new(Step::C, 4)),
duration: Duration::Quarter,
dot_count: 0,
is_rest: false,
tuplet: None,
}),
&mut score,
)
.unwrap();
apply_command(
&Command::SetFingerings(SetFingeringsCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
note_index: 0,
fingerings: vec![1, 3, 4],
}),
&mut score,
)
.unwrap();
let note = &score.parts[0].staves[0].measures[0].voices[0][0];
assert_eq!(note.fingerings, vec![1, 3, 4]);
assert_eq!(note.fingering, Some(1));
}
#[test]
fn set_figured_bass_replaces_measure_figures() {
let mut score = default_engine_score();
let figures = vec![FiguredBassFigure {
number: "6".to_string(),
alter: Some("-1".to_string()),
prefix: Some("+".to_string()),
suffix: None,
extender: false,
}];
apply_command(
&Command::SetFiguredBass(SetFiguredBassCmd {
measure_index: 0,
figures: figures.clone(),
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts[0].staves[0].measures[0].figured_bass, figures);
}
#[test]
fn set_measure_text_supports_append_replace_remove_and_undo_redo() {
let mut engine = crate::ScoreEngine::new();
let address = SetMeasureTextCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
text_index: 0,
text: Some(StyledText {
style: crate::TextStyle::Technique,
text: "dolce".to_string(),
placement: None,
offset_x: None,
offset_y: None,
relative_x: None,
relative_y: None,
}),
};
engine.apply(Command::SetMeasureText(address)).unwrap();
assert_eq!(
engine.score.parts[0].staves[0].measures[0].texts[0],
StyledText {
style: crate::TextStyle::Technique,
text: "dolce".to_string(),
placement: None,
offset_x: None,
offset_y: None,
relative_x: None,
relative_y: None,
}
);
engine
.apply(Command::SetMeasureText(SetMeasureTextCmd {
text_index: 0,
text: Some(StyledText {
style: crate::TextStyle::RehearsalMark,
text: "A".to_string(),
placement: None,
offset_x: None,
offset_y: None,
relative_x: None,
relative_y: None,
}),
..SetMeasureTextCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
text_index: 0,
text: None,
}
}))
.unwrap();
assert_eq!(
engine.score.parts[0].staves[0].measures[0].texts[0].style,
crate::TextStyle::RehearsalMark
);
engine
.apply(Command::SetMeasureText(SetMeasureTextCmd {
text_index: 0,
text: None,
..SetMeasureTextCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
text_index: 0,
text: None,
}
}))
.unwrap();
assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
engine.undo().unwrap();
assert_eq!(engine.score.parts[0].staves[0].measures[0].texts.len(), 1);
engine.redo().unwrap();
assert!(engine.score.parts[0].staves[0].measures[0].texts.is_empty());
}
#[test]
fn set_measure_text_rejects_invalid_index_atomically_and_round_trips_json() {
let mut score = default_engine_score();
let before = score.clone();
let command = Command::SetMeasureText(SetMeasureTextCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
text_index: 2,
text: Some(StyledText {
style: crate::TextStyle::Expression,
text: "espressivo".to_string(),
placement: None,
offset_x: None,
offset_y: None,
relative_x: None,
relative_y: None,
}),
});
let json = serde_json::to_string(&command).unwrap();
let restored: Command = serde_json::from_str(&json).unwrap();
assert_eq!(
serde_json::to_value(&restored).unwrap(),
serde_json::to_value(&command).unwrap()
);
assert!(apply_command(&restored, &mut score).is_err());
assert_eq!(
serde_json::to_value(&score).unwrap(),
serde_json::to_value(&before).unwrap()
);
}
#[test]
fn set_score_text_supports_append_replace_remove_and_undo_redo() {
let mut engine = crate::ScoreEngine::new();
let text = StyledText {
style: crate::TextStyle::Expression,
text: "Title".to_string(),
placement: None,
offset_x: None,
offset_y: None,
relative_x: None,
relative_y: None,
};
engine
.apply(Command::SetScoreText(SetScoreTextCmd {
text_index: 0,
text: Some(text.clone()),
}))
.unwrap();
assert_eq!(engine.score.texts, vec![text.clone()]);
let mut replacement = text.clone();
replacement.text = "Subtitle".to_string();
engine
.apply(Command::SetScoreText(SetScoreTextCmd {
text_index: 0,
text: Some(replacement.clone()),
}))
.unwrap();
assert_eq!(engine.score.texts, vec![replacement]);
engine
.apply(Command::SetScoreText(SetScoreTextCmd {
text_index: 0,
text: None,
}))
.unwrap();
assert!(engine.score.texts.is_empty());
engine.undo().unwrap();
assert_eq!(engine.score.texts.len(), 1);
engine.redo().unwrap();
assert!(engine.score.texts.is_empty());
}
#[test]
fn set_harmony_range_is_undoable_and_json_compatible() {
let mut score = default_engine_score();
score.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
score.parts[0].staves[0].measures[0].voices[0][0].chord_symbol = Some(ChordSymbol {
root: "C".to_owned(),
kind: "major".to_owned(),
bass: None,
placement: None,
extender: true,
harmonic_degree: None,
harmony_function: None,
harmony_type: None,
chord_ref: None,
range_end: None,
degrees: Vec::new(),
});
let mut engine = ScoreEngine::new();
engine.replace_score(score);
let command = Command::SetHarmonyRange(SetHarmonyRangeCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
note_index: 0,
end: Some(NoteAddr {
part: 0,
staff: 0,
measure: 0,
voice: 0,
note: 0,
}),
});
let json = serde_json::to_string(&command).unwrap();
let restored: Command = serde_json::from_str(&json).unwrap();
engine.apply(restored).unwrap();
assert!(
engine.score.parts[0].staves[0].measures[0].voices[0][0]
.chord_symbol
.as_ref()
.and_then(|chord| chord.range_end.as_ref())
.is_some()
);
engine.undo().unwrap();
assert!(
engine.score.parts[0].staves[0].measures[0].voices[0][0]
.chord_symbol
.as_ref()
.is_some_and(|chord| chord.range_end.is_none())
);
}
#[test]
fn set_note_placement_is_undoable_and_rejects_non_finite_values() {
let mut score = default_engine_score();
score.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
let mut engine = ScoreEngine::new();
engine.replace_score(score);
let command = Command::SetNotePlacement(SetNotePlacementCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
note_index: 0,
offset_x: Some(12.5),
offset_y: Some(-3.0),
relative_x: Some(1.25),
relative_y: Some(-0.5),
});
engine.apply(command).unwrap();
let note = &engine.score.parts[0].staves[0].measures[0].voices[0][0];
assert_eq!(note.offset_x, Some(12.5));
assert_eq!(note.relative_y, Some(-0.5));
engine.undo().unwrap();
assert_eq!(
engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
None
);
let invalid = Command::SetNotePlacement(SetNotePlacementCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
note_index: 0,
offset_x: Some(f64::NAN),
offset_y: None,
relative_x: None,
relative_y: None,
});
assert!(engine.apply(invalid).is_err());
assert_eq!(
engine.score.parts[0].staves[0].measures[0].voices[0][0].offset_x,
None
);
}
#[test]
fn set_guitar_bend_alter_updates_note_and_can_clear() {
let mut score = default_engine_score();
apply_command(
&Command::AddNote(AddNoteCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
position: 0,
pitch: Some(Pitch::new(Step::G, 4)),
duration: Duration::Quarter,
dot_count: 0,
is_rest: false,
tuplet: None,
}),
&mut score,
)
.unwrap();
let location = SetGuitarBendAlterCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
note_index: 0,
alter_cents: Some(200),
};
apply_command(&Command::SetGuitarBendAlter(location.clone()), &mut score).unwrap();
assert_eq!(
score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
Some(200)
);
let mut cleared = location;
cleared.alter_cents = None;
apply_command(&Command::SetGuitarBendAlter(cleared), &mut score).unwrap();
assert_eq!(
score.parts[0].staves[0].measures[0].voices[0][0].guitar_bend_alter_cents,
None
);
}
#[test]
fn set_duration_updates_note_and_preserves_measure_capacity() {
let mut score = default_engine_score();
apply_command(
&Command::AddNote(AddNoteCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
position: 0,
pitch: Some(Pitch::new(Step::C, 4)),
duration: Duration::Quarter,
dot_count: 0,
is_rest: false,
tuplet: None,
}),
&mut score,
)
.unwrap();
apply_command(
&Command::SetDuration(SetDurationCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
note_index: 0,
duration: Duration::Half,
dot_count: 1,
}),
&mut score,
)
.unwrap();
let voice = &score.parts[0].staves[0].measures[0].voices[0];
assert_eq!(voice[0].duration, Duration::Half);
assert_eq!(voice[0].dot_count, 1);
assert!((voice.iter().map(|note| note.beats()).sum::<f64>() - 4.0).abs() < 1e-9);
}
#[test]
fn set_tempo_updates_score() {
let mut score = default_engine_score();
let cmd = Command::SetTempo(SetTempoCmd { bpm: 160 });
apply_command(&cmd, &mut score).unwrap();
assert_eq!(score.settings.tempo_bpm, 160);
}
#[test]
fn add_measure_increases_count() {
let mut score = default_engine_score();
let before = score.measure_count();
apply_command(
&Command::AddMeasure(AddMeasureCmd { after_index: 0 }),
&mut score,
)
.unwrap();
assert_eq!(score.measure_count(), before + 1);
}
#[test]
fn delete_measure_decreases_count() {
let mut score = default_engine_score();
let before = score.measure_count();
apply_command(
&Command::DeleteMeasure(DeleteMeasureCmd { measure_index: 0 }),
&mut score,
)
.unwrap();
assert_eq!(score.measure_count(), before - 1);
}
#[test]
fn undo_restores_score() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
let before = score.settings.tempo_bpm;
stack
.execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
.unwrap();
assert_eq!(score.settings.tempo_bpm, 200);
stack.undo(&mut score).unwrap();
assert_eq!(score.settings.tempo_bpm, before);
}
#[test]
fn redo_reapplies_command() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
stack
.execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
.unwrap();
stack.undo(&mut score).unwrap();
stack.redo(&mut score).unwrap();
assert_eq!(score.settings.tempo_bpm, 200);
}
#[test]
fn undo_nothing_returns_error() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
assert!(stack.undo(&mut score).is_err());
}
#[test]
fn add_part_appends_part() {
let mut score = default_engine_score();
let before = score.parts.len();
apply_command(
&Command::AddPart(AddPartCmd {
name: "Violin".into(),
short_name: "Vln.".into(),
clefs: vec!["Treble".into()],
midi_channel: 0,
midi_program: 0,
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts.len(), before + 1);
}
#[test]
fn delete_part_removes_part() {
let mut score = default_engine_score();
apply_command(
&Command::AddPart(AddPartCmd {
name: "Violin".into(),
short_name: "V.".into(),
clefs: vec!["Treble".into()],
midi_channel: 0,
midi_program: 0,
}),
&mut score,
)
.unwrap();
let before = score.parts.len();
apply_command(
&Command::DeletePart(DeletePartCmd { part_index: 0 }),
&mut score,
)
.unwrap();
assert_eq!(score.parts.len(), before - 1);
}
#[test]
fn delete_part_out_of_range_returns_err() {
let mut score = default_engine_score();
assert!(
apply_command(
&Command::DeletePart(DeletePartCmd { part_index: 99 }),
&mut score
)
.is_err()
);
}
#[test]
fn delete_part_undo_restores_part() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
apply_command(
&Command::AddPart(AddPartCmd {
name: "Violin".into(),
short_name: "V.".into(),
clefs: vec!["Treble".into()],
midi_channel: 0,
midi_program: 0,
}),
&mut score,
)
.unwrap();
let before = score.parts.len();
stack
.execute(
Command::DeletePart(DeletePartCmd { part_index: 0 }),
&mut score,
)
.unwrap();
assert_eq!(score.parts.len(), before - 1);
stack.undo(&mut score).unwrap();
assert_eq!(score.parts.len(), before);
}
#[test]
fn set_metadata_updates_title() {
let mut score = default_engine_score();
apply_command(
&Command::SetMetadata(SetMetadataCmd {
title: Some("New Title".into()),
..Default::default()
}),
&mut score,
)
.unwrap();
assert_eq!(score.metadata.title, "New Title");
}
#[test]
fn set_metadata_none_fields_skipped() {
let mut score = default_engine_score();
let original_composer = score.metadata.composer.clone();
apply_command(
&Command::SetMetadata(SetMetadataCmd {
title: Some("X".into()),
..Default::default()
}),
&mut score,
)
.unwrap();
assert_eq!(score.metadata.composer, original_composer);
}
#[test]
fn set_volta_sets_bracket() {
use crate::model::score::VoltaBracket;
let mut score = default_engine_score();
let volta = VoltaBracket {
number: 1,
kind: "begin_end".into(),
};
apply_command(
&Command::SetVolta(SetVoltaCmd {
measure_index: 0,
volta: Some(volta.clone()),
}),
&mut score,
)
.unwrap();
assert!(score.parts[0].staves[0].measures[0].volta.is_some());
}
#[test]
fn set_volta_none_clears_bracket() {
use crate::model::score::VoltaBracket;
let mut score = default_engine_score();
score.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
number: 1,
kind: "begin_end".into(),
});
apply_command(
&Command::SetVolta(SetVoltaCmd {
measure_index: 0,
volta: None,
}),
&mut score,
)
.unwrap();
assert!(score.parts[0].staves[0].measures[0].volta.is_none());
}
#[test]
fn set_volta_undo_restores_old() {
use crate::model::score::VoltaBracket;
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
stack
.execute(
Command::SetVolta(SetVoltaCmd {
measure_index: 0,
volta: Some(VoltaBracket {
number: 1,
kind: "begin_end".into(),
}),
}),
&mut score,
)
.unwrap();
stack.undo(&mut score).unwrap();
assert!(score.parts[0].staves[0].measures[0].volta.is_none());
}
#[test]
fn set_clef_updates_staff_clef() {
use crate::model::notation::Clef;
let mut score = default_engine_score();
apply_command(
&Command::SetClef(SetClefCmd {
part_index: 0,
staff_index: 0,
clef: Clef::Bass,
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts[0].staves[0].clef, Clef::Bass);
}
#[test]
fn set_clef_out_of_range_returns_err() {
use crate::model::notation::Clef;
let mut score = default_engine_score();
assert!(
apply_command(
&Command::SetClef(SetClefCmd {
part_index: 99,
staff_index: 0,
clef: Clef::Bass,
}),
&mut score
)
.is_err()
);
}
#[test]
fn set_part_name_updates_name() {
let mut score = default_engine_score();
apply_command(
&Command::SetPartName(SetPartNameCmd {
part_index: 0,
name: "Violin".into(),
short_name: "Vln.".into(),
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts[0].name, "Violin");
assert_eq!(score.parts[0].short_name, "Vln.");
}
#[test]
fn set_part_name_undo_restores_old() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
let original = score.parts[0].name.clone();
stack
.execute(
Command::SetPartName(SetPartNameCmd {
part_index: 0,
name: "Flute".into(),
short_name: "Fl.".into(),
}),
&mut score,
)
.unwrap();
stack.undo(&mut score).unwrap();
assert_eq!(score.parts[0].name, original);
}
#[test]
fn set_metadata_undo_restores_old_title() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
let original = score.metadata.title.clone();
stack
.execute(
Command::SetMetadata(SetMetadataCmd {
title: Some("Changed".into()),
..Default::default()
}),
&mut score,
)
.unwrap();
assert_ne!(score.metadata.title, original);
stack.undo(&mut score).unwrap();
assert_eq!(score.metadata.title, original);
}
#[test]
fn set_midi_instrument_updates_channel_and_program() {
let mut score = default_engine_score();
apply_command(
&Command::SetMidiInstrument(SetMidiInstrumentCmd {
part_index: 0,
midi_channel: 2,
midi_program: 40,
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts[0].midi_channel, 2);
assert_eq!(score.parts[0].midi_program, 40);
}
#[test]
fn set_midi_instrument_clamps_channel_to_15() {
let mut score = default_engine_score();
apply_command(
&Command::SetMidiInstrument(SetMidiInstrumentCmd {
part_index: 0,
midi_channel: 20,
midi_program: 0,
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts[0].midi_channel, 15);
}
#[test]
fn set_midi_instrument_undo_restores_old() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
score.parts[0].midi_channel = 3;
score.parts[0].midi_program = 10;
stack
.execute(
Command::SetMidiInstrument(SetMidiInstrumentCmd {
part_index: 0,
midi_channel: 9,
midi_program: 114,
}),
&mut score,
)
.unwrap();
stack.undo(&mut score).unwrap();
assert_eq!(score.parts[0].midi_channel, 3);
assert_eq!(score.parts[0].midi_program, 10);
}
#[test]
fn set_transpose_updates_staff() {
let mut score = default_engine_score();
apply_command(
&Command::SetTranspose(SetTransposeCmd {
part_index: 0,
staff_index: 0,
semitones: -2,
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts[0].staves[0].transpose_semitones, -2);
}
#[test]
fn set_transpose_out_of_range_returns_err() {
let mut score = default_engine_score();
assert!(
apply_command(
&Command::SetTranspose(SetTransposeCmd {
part_index: 99,
staff_index: 0,
semitones: -2,
}),
&mut score
)
.is_err()
);
}
#[test]
fn set_tempo_at_measure_sets_tempo() {
let mut score = default_engine_score();
apply_command(
&Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
measure_index: 0,
bpm: Some(80),
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts[0].staves[0].measures[0].tempo, Some(80));
}
#[test]
fn set_tempo_at_measure_none_clears_tempo() {
let mut score = default_engine_score();
score.parts[0].staves[0].measures[0].tempo = Some(120);
apply_command(
&Command::SetTempoAtMeasure(SetTempoAtMeasureCmd {
measure_index: 0,
bpm: None,
}),
&mut score,
)
.unwrap();
assert!(score.parts[0].staves[0].measures[0].tempo.is_none());
}
#[test]
fn batch_execute_two_commands_single_undo() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
let original_bpm = score.settings.tempo_bpm;
stack
.batch_execute(
vec![
Command::SetTempo(SetTempoCmd { bpm: 160 }),
Command::SetTempo(SetTempoCmd { bpm: 180 }),
],
&mut score,
)
.unwrap();
assert_eq!(score.settings.tempo_bpm, 180);
stack.undo(&mut score).unwrap();
assert_eq!(score.settings.tempo_bpm, original_bpm);
}
#[test]
fn batch_execute_partial_failure_rollback() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
let original_bpm = score.settings.tempo_bpm;
let result = stack.batch_execute(
vec![
Command::SetTempo(SetTempoCmd { bpm: 160 }),
Command::DeleteNote(DeleteNoteCmd {
note_id: "nonexistent".into(),
part_index: 99,
staff_index: 0,
measure_index: 0,
voice: 0,
}),
],
&mut score,
);
assert!(result.is_err());
assert_eq!(score.settings.tempo_bpm, original_bpm);
}
#[test]
fn execute_rejects_invalid_candidate_without_mutating_or_recording_history() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
score.parts[0].staves[0].tablature = Some(crate::TablatureConfig {
lines: 6,
tuning_midi: vec![40, 45, 50, 55, 59, 64],
capo: 0,
});
stack
.execute(
Command::AddNote(AddNoteCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
position: 0,
pitch: Some(Pitch::new(Step::C, 4)),
duration: Duration::Quarter,
dot_count: 0,
is_rest: false,
tuplet: None,
}),
&mut score,
)
.expect("valid pitched note should be accepted");
let original = score.clone();
let result = stack.execute(
Command::SetTabPosition(SetTabPositionCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
note_index: 0,
position: Some(crate::TabPosition { string: 7, fret: 0 }),
}),
&mut score,
);
assert!(matches!(result, Err(Error::InvalidScore)));
let note = &score.parts[0].staves[0].measures[0].voices[0][0];
let original_note = &original.parts[0].staves[0].measures[0].voices[0][0];
assert_eq!(note.pitches, original_note.pitches);
assert_eq!(note.duration, original_note.duration);
assert_eq!(note.tab_position, original_note.tab_position);
assert_eq!(note.tab_positions, original_note.tab_positions);
assert!(stack.can_undo());
}
#[test]
fn set_tablature_config_is_undoable_and_json_compatible() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
let config = crate::TablatureConfig {
lines: 6,
tuning_midi: vec![40, 45, 50, 55, 59, 64],
capo: 2,
};
let command = Command::SetTablatureConfig(SetTablatureConfigCmd {
part_index: 0,
staff_index: 0,
config: Some(config.clone()),
});
let json = serde_json::to_string(&command).expect("command should serialize");
let decoded: Command = serde_json::from_str(&json).expect("command should deserialize");
assert_eq!(command_key(&decoded), "SetTablatureConfig");
stack
.execute(decoded, &mut score)
.expect("config should apply");
assert_eq!(score.parts[0].staves[0].tablature, Some(config));
stack.undo(&mut score).expect("config undo should apply");
assert!(score.parts[0].staves[0].tablature.is_none());
stack.redo(&mut score).expect("config redo should apply");
assert_eq!(
score.parts[0].staves[0]
.tablature
.as_ref()
.map(|tab| tab.capo),
Some(2)
);
}
#[test]
fn batch_execute_empty_is_noop() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
stack.batch_execute(vec![], &mut score).unwrap();
assert!(!stack.can_undo());
}
#[test]
fn batch_label_used_as_command_key() {
let cmd = Command::Batch(BatchCmd {
commands: vec![],
label: Some("ApplyAI".to_string()),
});
assert_eq!(command_key(&cmd), "ApplyAI");
}
#[test]
fn batch_no_label_key_is_batch() {
let cmd = Command::Batch(BatchCmd {
commands: vec![],
label: None,
});
assert_eq!(command_key(&cmd), "Batch");
}
#[test]
fn batch_label_survives_json_roundtrip() {
let cmd = Command::Batch(BatchCmd {
commands: vec![Command::SetTempo(SetTempoCmd { bpm: 120 })],
label: Some("PasteSelection".to_string()),
});
let json = serde_json::to_string(&cmd).unwrap();
let cmd2: Command = serde_json::from_str(&json).unwrap();
assert_eq!(command_key(&cmd2), "PasteSelection");
}
#[test]
fn batch_label_in_undo_key() {
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
let cmd = Command::Batch(BatchCmd {
commands: vec![Command::SetTempo(SetTempoCmd { bpm: 140 })],
label: Some("ApplyAI".to_string()),
});
stack.execute(cmd, &mut score).unwrap();
assert_eq!(stack.undo_key(), Some("ApplyAI".to_string()));
}
#[test]
fn undo_returns_change_hint() {
use crate::model::change_hint::ChangeScope;
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
stack
.execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
.unwrap();
let hint = stack.undo(&mut score).unwrap();
assert_eq!(hint.scope, ChangeScope::Global);
assert!(hint.playback_dirty);
}
#[test]
fn redo_returns_change_hint() {
use crate::model::change_hint::ChangeScope;
let mut stack = CommandStack::new(50);
let mut score = default_engine_score();
stack
.execute(Command::SetTempo(SetTempoCmd { bpm: 200 }), &mut score)
.unwrap();
stack.undo(&mut score).unwrap();
let hint = stack.redo(&mut score).unwrap();
assert_eq!(hint.scope, ChangeScope::Global);
assert!(hint.playback_dirty);
}
#[test]
fn toggle_slur_sets_start_and_end() {
let mut score = default_engine_score();
let cmd = Command::AddNote(AddNoteCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
position: 0,
pitch: Some(Pitch::new(Step::C, 4)),
duration: Duration::Quarter,
dot_count: 0,
is_rest: false,
tuplet: None,
});
apply_command(&cmd, &mut score).unwrap();
apply_command(
&Command::AddNote(AddNoteCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
position: 1,
pitch: Some(Pitch::new(Step::D, 4)),
duration: Duration::Quarter,
dot_count: 0,
is_rest: false,
tuplet: None,
}),
&mut score,
)
.unwrap();
let start = NoteAddr {
part: 0,
staff: 0,
measure: 0,
voice: 0,
note: 0,
};
let end = NoteAddr {
part: 0,
staff: 0,
measure: 0,
voice: 0,
note: 1,
};
apply_command(
&Command::ToggleSlur(ToggleSlurCmd {
start: start.clone(),
end: end.clone(),
}),
&mut score,
)
.unwrap();
assert!(score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
assert!(score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
apply_command(
&Command::ToggleSlur(ToggleSlurCmd { start, end }),
&mut score,
)
.unwrap();
assert!(!score.parts[0].staves[0].measures[0].voices[0][0].slur_start);
assert!(!score.parts[0].staves[0].measures[0].voices[0][1].slur_end);
}
#[test]
fn add_staff_appends_staff_with_correct_measure_count() {
let mut score = default_engine_score();
let before = score.parts[0].staves.len();
let measure_count = score.parts[0].staves[0].measures.len();
apply_command(
&Command::AddStaff(AddStaffCmd {
part_index: 0,
clef: Clef::Bass,
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts[0].staves.len(), before + 1);
let new_staff = score.parts[0].staves.last().unwrap();
assert_eq!(new_staff.measures.len(), measure_count);
}
#[test]
fn add_staff_out_of_range_returns_err() {
let mut score = default_engine_score();
let result = apply_command(
&Command::AddStaff(AddStaffCmd {
part_index: 99,
clef: Clef::Treble,
}),
&mut score,
);
assert!(result.is_err());
}
#[test]
fn delete_staff_removes_extra_staff() {
let mut score = default_engine_score();
apply_command(
&Command::AddStaff(AddStaffCmd {
part_index: 0,
clef: Clef::Bass,
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts[0].staves.len(), 2);
apply_command(
&Command::DeleteStaff(DeleteStaffCmd {
part_index: 0,
staff_index: 1,
}),
&mut score,
)
.unwrap();
assert_eq!(score.parts[0].staves.len(), 1);
}
#[test]
fn delete_last_staff_returns_err() {
let mut score = default_engine_score();
assert_eq!(score.parts[0].staves.len(), 1);
let result = apply_command(
&Command::DeleteStaff(DeleteStaffCmd {
part_index: 0,
staff_index: 0,
}),
&mut score,
);
assert!(result.is_err());
}
#[test]
fn set_tuplet_assigns_and_clears() {
use crate::model::notation::TupletInfo;
let mut score = default_engine_score();
apply_command(
&Command::AddNote(AddNoteCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
position: 0,
pitch: Some(Pitch::new(Step::C, 4)),
duration: Duration::Quarter,
dot_count: 0,
is_rest: false,
tuplet: None,
}),
&mut score,
)
.unwrap();
let ti = TupletInfo {
actual_notes: 3,
normal_notes: 2,
};
apply_command(
&Command::SetTuplet(SetTupletCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice_index: 0,
note_index: 0,
tuplet: Some(ti.clone()),
}),
&mut score,
)
.unwrap();
assert_eq!(
score.parts[0].staves[0].measures[0].voices[0][0].tuplet,
Some(ti)
);
apply_command(
&Command::SetTuplet(SetTupletCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice_index: 0,
note_index: 0,
tuplet: None,
}),
&mut score,
)
.unwrap();
assert!(
score.parts[0].staves[0].measures[0].voices[0][0]
.tuplet
.is_none()
);
}
#[test]
fn respell_score_cmd_changes_all_pitches() {
use crate::model::pitch::Step;
let mut score = default_engine_score();
apply_command(
&Command::AddNote(AddNoteCmd {
part_index: 0,
staff_index: 0,
measure_index: 0,
voice: 0,
position: 0,
pitch: Some(Pitch::with_alter(Step::C, 4, 1)), duration: Duration::Quarter,
dot_count: 0,
is_rest: false,
tuplet: None,
}),
&mut score,
)
.unwrap();
apply_command(
&Command::RespellScore(RespellScoreCmd { prefer_flat: true }),
&mut score,
)
.unwrap();
let pitch = &score.parts[0].staves[0].measures[0].voices[0][0].pitches[0];
assert_eq!(pitch.step, Step::D);
assert_eq!(pitch.alter, -1); }
#[test]
fn typed_spanner_commands_are_atomic_and_undoable() {
use crate::model::score::{NotationSpanner, NotationSpannerKind};
let mut score = default_engine_score();
let address = NoteAddr {
part: 0,
staff: 0,
measure: 0,
voice: 0,
note: 0,
};
let spanner = NotationSpanner {
id: "slur-1".to_string(),
kind: NotationSpannerKind::Slur,
start: address.clone(),
end: address,
number: Some(1),
line_type: Some("dashed".to_string()),
text: None,
placement: Some("above".to_string()),
ottava_size: None,
ottava_type: None,
};
let mut stack = CommandStack::new(8);
stack
.execute(Command::AddSpanner(AddSpannerCmd { spanner }), &mut score)
.unwrap();
assert_eq!(score.spanners.len(), 1);
assert_eq!(stack.undo_key().as_deref(), Some("AddSpanner"));
let before_duplicate = score.clone();
let duplicate = score.spanners[0].clone();
assert!(
stack
.execute(
Command::AddSpanner(AddSpannerCmd { spanner: duplicate }),
&mut score,
)
.is_err()
);
assert_eq!(score.spanners, before_duplicate.spanners);
let mut updated = score.spanners[0].clone();
updated.number = Some(2);
stack
.execute(
Command::UpdateSpanner(UpdateSpannerCmd { spanner: updated }),
&mut score,
)
.unwrap();
assert_eq!(score.spanners[0].number, Some(2));
stack.undo(&mut score).unwrap();
assert_eq!(score.spanners[0].number, Some(1));
stack.redo(&mut score).unwrap();
assert_eq!(score.spanners[0].number, Some(2));
stack
.execute(
Command::RemoveSpanner(RemoveSpannerCmd {
id: "slur-1".to_string(),
}),
&mut score,
)
.unwrap();
assert!(score.spanners.is_empty());
stack.undo(&mut score).unwrap();
assert_eq!(score.spanners.len(), 1);
}
}