sim_lib_music_serial/
additive.rs1use std::collections::BTreeSet;
4
5use sim_lib_music_core::{Staff, StaffNote, StaffVoice};
6
7#[derive(Clone, Debug, Default, PartialEq, Eq)]
9pub struct AdditiveStaffPatch {
10 pub voices: Vec<StaffVoice>,
12 pub notes: Vec<StaffNote>,
14}
15
16pub(crate) fn apply_additive_staff_patch(
17 source: &Staff,
18 patch: &AdditiveStaffPatch,
19) -> Result<Staff, String> {
20 let source_ids = source.object_ids().into_iter().collect::<BTreeSet<_>>();
21 let mut voices = source.voices.clone();
22
23 for voice in &patch.voices {
24 if source_ids.contains(&voice.id) || voices.iter().any(|item| item.id == voice.id) {
25 return Err("an added voice identity already exists".to_owned());
26 }
27 voices.push(voice.clone());
28 }
29
30 for note in &patch.notes {
31 let Some(voice) = voices.iter_mut().find(|voice| voice.id == note.voice_id) else {
32 return Err("an added note names a missing voice".to_owned());
33 };
34 voice.notes.push(note.clone());
35 }
36
37 let completed = Staff::new(voices).map_err(|err| err.to_string())?;
38 ensure_source_unchanged(source, &completed)?;
39 Ok(completed)
40}
41
42pub(crate) fn remove_additive_staff_patch(
43 completed: &Staff,
44 patch: &AdditiveStaffPatch,
45) -> Result<Staff, String> {
46 let mut voices = completed.voices.clone();
47
48 for note in patch.notes.iter().rev() {
49 let Some(voice) = voices.iter_mut().find(|voice| voice.id == note.voice_id) else {
50 return Err("the completed staff is missing an added note voice".to_owned());
51 };
52 let Some(index) = voice.notes.iter().position(|candidate| candidate == note) else {
53 return Err("an added note is missing or has changed".to_owned());
54 };
55 voice.notes.remove(index);
56 }
57
58 for added in patch.voices.iter().rev() {
59 let Some(index) = voices.iter().position(|voice| voice.id == added.id) else {
60 return Err("an added voice is missing".to_owned());
61 };
62 if voices[index] != *added {
63 return Err("an added voice has changed".to_owned());
64 }
65 voices.remove(index);
66 }
67
68 Staff::new(voices).map_err(|err| err.to_string())
69}
70
71fn ensure_source_unchanged(source: &Staff, completed: &Staff) -> Result<(), String> {
72 for source_voice in &source.voices {
73 let Some(completed_voice) = completed
74 .voices
75 .iter()
76 .find(|voice| voice.id == source_voice.id)
77 else {
78 return Err("an additive transform removed a source voice".to_owned());
79 };
80 if completed_voice.name != source_voice.name
81 || completed_voice.duration != source_voice.duration
82 || !source_voice
83 .notes
84 .iter()
85 .all(|note| completed_voice.notes.contains(note))
86 {
87 return Err("an additive transform changed source material".to_owned());
88 }
89 }
90 Ok(())
91}