sim_lib_music_transform/exact/
additive.rs1use std::collections::BTreeSet;
2
3use sim_lib_music_core::{Staff, StaffNote, StaffVoice};
4
5use super::{MusicTransform, MusicTransformChange, finish};
6use crate::TransformError;
7
8#[derive(Clone, Debug, Default, PartialEq, Eq)]
14pub struct AdditiveStaffPatch {
15 pub voices: Vec<StaffVoice>,
17 pub notes: Vec<StaffNote>,
19}
20
21pub fn apply_additive_staff_patch(
23 source: &Staff,
24 patch: &AdditiveStaffPatch,
25) -> Result<MusicTransform<Staff>, TransformError> {
26 let source_ids = source.object_ids().into_iter().collect::<BTreeSet<_>>();
27 let mut voices = source.voices.clone();
28 let mut changes = Vec::new();
29
30 for voice in &patch.voices {
31 if source_ids.contains(&voice.id) || voices.iter().any(|item| item.id == voice.id) {
32 return invalid("an added voice identity already exists");
33 }
34 changes.push(MusicTransformChange::AddedVoice {
35 voice_id: voice.id.clone(),
36 });
37 for note in &voice.notes {
38 changes.push(added_note_change(note));
39 }
40 voices.push(voice.clone());
41 }
42
43 for note in &patch.notes {
44 let Some(voice) = voices.iter_mut().find(|voice| voice.id == note.voice_id) else {
45 return invalid("an added note names a missing voice");
46 };
47 changes.push(added_note_change(note));
48 voice.notes.push(note.clone());
49 }
50
51 let transformed = finish(voices, changes)?;
52 ensure_source_unchanged(source, &transformed.value)?;
53 Ok(transformed)
54}
55
56pub fn remove_additive_staff_patch(
62 completed: &Staff,
63 patch: &AdditiveStaffPatch,
64) -> Result<MusicTransform<Staff>, TransformError> {
65 let mut voices = completed.voices.clone();
66 let mut changes = Vec::new();
67
68 for note in patch.notes.iter().rev() {
69 let Some(voice) = voices.iter_mut().find(|voice| voice.id == note.voice_id) else {
70 return invalid("the completed staff is missing an added note voice");
71 };
72 let Some(index) = voice.notes.iter().position(|candidate| candidate == note) else {
73 return invalid("an added note is missing or has changed");
74 };
75 voice.notes.remove(index);
76 changes.push(MusicTransformChange::Removed {
77 note_id: note.note_id.clone(),
78 event_id: note.event_id.clone(),
79 reason: "reverse additive staff patch",
80 });
81 }
82
83 for added in patch.voices.iter().rev() {
84 let Some(index) = voices.iter().position(|voice| voice.id == added.id) else {
85 return invalid("an added voice is missing");
86 };
87 if voices[index] != *added {
88 return invalid("an added voice has changed");
89 }
90 for note in &added.notes {
91 changes.push(MusicTransformChange::Removed {
92 note_id: note.note_id.clone(),
93 event_id: note.event_id.clone(),
94 reason: "reverse additive staff patch",
95 });
96 }
97 changes.push(MusicTransformChange::RemovedVoice {
98 voice_id: added.id.clone(),
99 });
100 voices.remove(index);
101 }
102
103 finish(voices, changes)
104}
105
106fn added_note_change(note: &StaffNote) -> MusicTransformChange {
107 MusicTransformChange::AddedNote {
108 voice_id: note.voice_id.clone(),
109 note_id: note.note_id.clone(),
110 event_id: note.event_id.clone(),
111 }
112}
113
114fn ensure_source_unchanged(source: &Staff, completed: &Staff) -> Result<(), TransformError> {
115 for source_voice in &source.voices {
116 let Some(completed_voice) = completed
117 .voices
118 .iter()
119 .find(|voice| voice.id == source_voice.id)
120 else {
121 return invalid("an additive transform removed a source voice");
122 };
123 if completed_voice.name != source_voice.name
124 || completed_voice.duration != source_voice.duration
125 || !source_voice
126 .notes
127 .iter()
128 .all(|note| completed_voice.notes.contains(note))
129 {
130 return invalid("an additive transform changed source material");
131 }
132 }
133 Ok(())
134}
135
136fn invalid<T>(reason: &'static str) -> Result<T, TransformError> {
137 Err(TransformError::InvalidTransformOutput {
138 transform: "additive-staff-patch",
139 reason,
140 })
141}