gled-protocol 2.30.2

What gled's user interfaces and its engine send each other
Documentation
//! Project changes on the wire. A whole project is around 50 KB, mostly the
//! SVG and the scene grid; an edit usually touches one scene. So after the
//! first full state, an interface gets only the scenes that changed, the SVG
//! when it changed, and the rest of the project when any of it changed.

use std::collections::HashMap;

use gled_model::asset::{
    project::Project,
    scene::{grid::GridLocation, instance::SceneInstance},
    svg::Svg,
};
use serde::{Deserialize, Serialize};

/// How a project replica becomes the engine's project.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ProjectUpdate {
    Unchanged,
    /// Another project, or none.
    Replaced(Option<Box<Project>>),
    Patched(ProjectPatch),
}

/// The differences between two projects.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProjectPatch {
    /// Scene instances that changed, `None` where one was removed.
    pub scenes: Vec<(GridLocation, Option<SceneInstance>)>,
    /// The new SVG, if it changed.
    pub svg: Option<Option<Svg>>,
    /// Everything but the scenes and the SVG, if any of it changed. Its own
    /// scenes and SVG are empty.
    pub rest: Option<Box<Project>>,
}

impl ProjectUpdate {
    /// What turns `old` into `new`.
    pub fn between(old: Option<&Project>, new: Option<&Project>) -> Self {
        match (old, new) {
            (None, None) => Self::Unchanged,
            (_, None) => Self::Replaced(None),
            (None, Some(new)) => Self::Replaced(Some(Box::new(new.clone()))),
            (Some(old), Some(new)) if old == new => Self::Unchanged,
            (Some(old), Some(new)) => Self::Patched(ProjectPatch::between(old, new)),
        }
    }

    /// Applies the update to a replica that was equal to the `old` project
    /// it was made from.
    pub fn apply_to(self, project: &mut Option<Project>) {
        match self {
            Self::Unchanged => {}
            Self::Replaced(new) => *project = new.map(|new| *new),
            Self::Patched(patch) => {
                if let Some(project) = project {
                    patch.apply_to(project);
                }
            }
        }
    }
}

impl ProjectPatch {
    fn between(old: &Project, new: &Project) -> Self {
        let mut scenes: Vec<_> = new
            .scenes_instances_grid
            .iter()
            .filter(|(location, scene)| old.scenes_instances_grid.get(location) != Some(scene))
            .map(|(location, scene)| (*location, Some(scene.clone())))
            .collect();
        scenes.extend(
            old.scenes_instances_grid
                .keys()
                .filter(|location| !new.scenes_instances_grid.contains_key(location))
                .map(|location| (*location, None)),
        );
        let svg = (old.svg != new.svg).then(|| new.svg.clone());
        let new_rest = rest(new);
        let rest = (rest(old) != new_rest).then(|| Box::new(new_rest));
        Self { scenes, svg, rest }
    }

    fn apply_to(self, project: &mut Project) {
        if let Some(rest) = self.rest {
            let scenes = std::mem::take(&mut project.scenes_instances_grid);
            let svg = project.svg.take();
            *project = *rest;
            project.scenes_instances_grid = scenes;
            project.svg = svg;
        }
        if let Some(svg) = self.svg {
            project.svg = svg;
        }
        for (location, scene) in self.scenes {
            match scene {
                Some(scene) => {
                    project.scenes_instances_grid.insert(location, scene);
                }
                None => {
                    project.scenes_instances_grid.remove(&location);
                }
            }
        }
    }
}

/// The project without its scenes and SVG. Listing every field makes a new
/// one a compile error here instead of a change that never reaches the
/// interfaces.
fn rest(project: &Project) -> Project {
    let Project {
        palette,
        groups,
        grid_width,
        grid_height,
        grid_highlight,
        scenes_instances_grid: _,
        svg: _,
        channel_overwrites,
        output_routings,
        artnet_config,
        tap_input_events,
        blackout_input_events,
        blackout_hold_input_events,
        half_input_events,
        double_input_events,
        midi_active_mappings,
        main_dimmer,
        audio_input_device,
        artnet_control_config,
        osc_config,
    } = project;
    Project {
        palette: palette.clone(),
        groups: groups.clone(),
        grid_width: *grid_width,
        grid_height: *grid_height,
        grid_highlight: *grid_highlight,
        channel_overwrites: channel_overwrites.clone(),
        output_routings: output_routings.clone(),
        artnet_config: artnet_config.clone(),
        tap_input_events: tap_input_events.clone(),
        blackout_input_events: blackout_input_events.clone(),
        blackout_hold_input_events: blackout_hold_input_events.clone(),
        half_input_events: half_input_events.clone(),
        double_input_events: double_input_events.clone(),
        midi_active_mappings: midi_active_mappings.clone(),
        main_dimmer: *main_dimmer,
        audio_input_device: audio_input_device.clone(),
        artnet_control_config: *artnet_control_config,
        osc_config: *osc_config,
        scenes_instances_grid: HashMap::new(),
        svg: None,
    }
}