gled-core 2.31.2

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
//! The lamp layout of the project's patch, followed by the engine whenever the
//! patch is replaced or a fixture definition it uses changes, so outputs work
//! without a window that draws it.

use std::sync::Arc;

use gled_model::{
    asset::Asset,
    collection::Collections,
    patching::{Patch, definition::FixtureDefinition},
};

use crate::pipeline::extract_output::OutputConfig;

#[derive(Debug, Default)]
pub(crate) struct PatchLayout {
    /// Counts the patches the engine put in place (`patch_replaced`), so a
    /// new one is noticed without comparing it to the old one every tick.
    patch_generation: u64,
    /// The patch generation the current layout was derived from; `None`
    /// before the first.
    derived_from: Option<u64>,
    /// The fixture definitions it was derived with, as shared by the
    /// collections: an edited definition is a new `Arc`.
    definitions: Vec<Arc<Asset<FixtureDefinition>>>,
    /// The collections' generation the definitions were last looked at in, so
    /// they are only compared again after some asset changed.
    collections_generation: Option<u64>,
    /// Counts the layouts applied, so interfaces notice a new one.
    generation: u64,
}

impl PatchLayout {
    /// Derives the output universes from `patch` if it was replaced or one of
    /// the fixture definitions differs from the ones used last, or clears them
    /// without a patch. Returns whether the lamp positions changed.
    pub(crate) fn update(
        &mut self,
        patch: Option<&Patch>,
        collections: &Collections,
        collections_generation: u64,
        output_config: &mut OutputConfig,
    ) -> bool {
        let definitions_changed = self.collections_generation != Some(collections_generation)
            && self.take_definitions(collections);
        self.collections_generation = Some(collections_generation);
        if !definitions_changed && self.derived_from == Some(self.patch_generation) {
            return false;
        }

        self.derived_from = Some(self.patch_generation);
        self.generation += 1;
        // Set together so the universes and the order of their lamps never
        // describe two different patches.
        output_config.universes = Arc::new(
            patch
                .map(|patch| {
                    patch
                        .addressing(collections)
                        .universes()
                        .iter()
                        .copied()
                        .collect()
                })
                .unwrap_or_default(),
        );
        output_config.color_orders = Arc::new(
            patch
                .map(|patch| patch.color_orders(collections))
                .unwrap_or_default(),
        );
        true
    }

    /// Keeps the current fixture definitions. Returns whether any differ from
    /// the ones kept before.
    fn take_definitions(&mut self, collections: &Collections) -> bool {
        let definitions: Vec<_> = collections
            .get::<FixtureDefinition>()
            .map(|collection| collection.shared().cloned().collect())
            .unwrap_or_default();
        let changed = definitions.len() != self.definitions.len()
            || definitions
                .iter()
                .zip(&self.definitions)
                .any(|(new, old)| !Arc::ptr_eq(new, old));
        self.definitions = definitions;
        changed
    }

    /// Notes that the project or its patch was replaced, so the next `update`
    /// derives the layout again. Every change of the engine's patch must call
    /// this; `update` does not look at the patch otherwise.
    pub(crate) fn patch_replaced(&mut self) {
        self.patch_generation += 1;
    }

    pub(crate) fn generation(&self) -> u64 {
        self.generation
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use gled_model::{
        asset_id::AssetId,
        collection::Collection,
        patching::{
            definition::{ColorOrder, EmittedColor, Pixel},
            fixture::{Fixture, LayoutPosition},
        },
    };

    fn collections_with_a_lamp() -> (Collections, AssetId<FixtureDefinition>) {
        let definition = FixtureDefinition {
            pixels: vec![Pixel {
                name: "lamp".to_owned(),
                model: 0,
                position_m: [0.0; 3],
                rotation: 0.0,
                channels: vec![EmittedColor::Red, EmittedColor::Green, EmittedColor::Blue],
            }],
            ..Default::default()
        };
        let asset = Asset {
            id: AssetId::default(),
            path: vec!["lamp".to_owned()],
            data: definition,
        };
        let id = asset.id;
        let mut collection = Collection::<FixtureDefinition>::default();
        collection.set_asset(asset);
        let mut collections = Collections::default();
        collections.insert(collection);
        (collections, id)
    }

    fn patch_with(definition: AssetId<FixtureDefinition>) -> Patch {
        let mut patch = Patch::default();
        let mut fixture = Fixture::new("lamp");
        fixture.definition = Some(definition);
        patch.add_fixture(fixture, LayoutPosition::default());
        patch
    }

    #[test]
    fn a_new_patch_sets_the_universes_once() {
        let (collections, definition) = collections_with_a_lamp();
        let patch = patch_with(definition);
        let mut layout = PatchLayout::default();
        let mut output = OutputConfig::default();

        assert!(layout.update(Some(&patch), &collections, 1, &mut output));
        assert_eq!(*output.universes, [0].into());
        assert!(
            !layout.update(Some(&patch), &collections, 1, &mut output),
            "nothing changed"
        );
    }

    #[test]
    fn an_unrelated_asset_change_keeps_the_layout() {
        let (collections, definition) = collections_with_a_lamp();
        let patch = patch_with(definition);
        let mut layout = PatchLayout::default();
        let mut output = OutputConfig::default();
        layout.update(Some(&patch), &collections, 1, &mut output);

        assert!(!layout.update(Some(&patch), &collections, 2, &mut output));
    }

    #[test]
    fn an_edited_definition_derives_the_layout_again() {
        let (mut collections, definition) = collections_with_a_lamp();
        let patch = patch_with(definition);
        let mut layout = PatchLayout::default();
        let mut output = OutputConfig::default();
        layout.update(Some(&patch), &collections, 1, &mut output);

        let mut edited = (**collections
            .get::<FixtureDefinition>()
            .and_then(|collection| collection.get(&definition))
            .expect("the definition is there"))
        .clone();
        edited.data.size_m = [1.0; 3];
        collections.get_mut::<FixtureDefinition>().set_asset(edited);

        assert!(layout.update(Some(&patch), &collections, 2, &mut output));
    }

    /// Editing a lamp into GRB reaches the output's colour orders with the
    /// next update, as its universes would.
    #[test]
    fn a_definition_edited_to_grb_reorders_its_lamp() {
        let (mut collections, definition) = collections_with_a_lamp();
        let patch = patch_with(definition);
        let mut layout = PatchLayout::default();
        let mut output = OutputConfig::default();
        layout.update(Some(&patch), &collections, 1, &mut output);
        assert!(output.color_orders.is_empty());

        let mut edited = (**collections
            .get::<FixtureDefinition>()
            .and_then(|collection| collection.get(&definition))
            .expect("the definition is there"))
        .clone();
        edited.data.pixels[0].set_color_order(ColorOrder::Grb);
        collections.get_mut::<FixtureDefinition>().set_asset(edited);
        layout.update(Some(&patch), &collections, 2, &mut output);

        assert_eq!(output.color_orders.order(0, 0), ColorOrder::Grb);
    }

    #[test]
    fn closing_the_project_clears_the_universes() {
        let (collections, definition) = collections_with_a_lamp();
        let patch = patch_with(definition);
        let mut layout = PatchLayout::default();
        let mut output = OutputConfig::default();
        layout.update(Some(&patch), &collections, 1, &mut output);

        layout.patch_replaced();
        assert!(layout.update(None, &collections, 1, &mut output));
        assert!(output.universes.is_empty());
    }

    #[test]
    fn a_replaced_patch_is_derived_again() {
        let (collections, definition) = collections_with_a_lamp();
        let mut layout = PatchLayout::default();
        let mut output = OutputConfig::default();
        layout.update(Some(&Patch::default()), &collections, 1, &mut output);
        assert!(output.universes.is_empty());
        let generation = layout.generation();

        layout.patch_replaced();
        let patch = patch_with(definition);
        assert!(layout.update(Some(&patch), &collections, 1, &mut output));
        assert_eq!(*output.universes, [0].into());
        assert_ne!(layout.generation(), generation, "interfaces notice it");
    }

    /// The engine announces every new patch, so a patch that was not
    /// announced is not compared and stays as derived.
    #[test]
    fn only_a_replaced_patch_is_derived_again() {
        let (collections, definition) = collections_with_a_lamp();
        let mut layout = PatchLayout::default();
        let mut output = OutputConfig::default();
        layout.update(Some(&Patch::default()), &collections, 1, &mut output);

        let patch = patch_with(definition);
        assert!(!layout.update(Some(&patch), &collections, 1, &mut output));
        assert!(output.universes.is_empty());
    }
}