Skip to main content

sim_lib_music_serial/
additive.rs

1//! Local reversible additive staff patching retained inside serial.
2
3use std::collections::BTreeSet;
4
5use sim_lib_music_core::{Staff, StaffNote, StaffVoice};
6
7/// Material introduced by a strictly additive staff transform.
8#[derive(Clone, Debug, Default, PartialEq, Eq)]
9pub struct AdditiveStaffPatch {
10    /// Complete voices introduced by the patch.
11    pub voices: Vec<StaffVoice>,
12    /// Notes introduced into named voices.
13    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}