gled-core 2.34.0

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
//! The loaded project: putting it in place, taking it out, and keeping what
//! follows its settings in line with it.

use std::sync::Arc;

use gled_model::{asset::scene::grid::GridLocation, patching::Patch};

use super::Engine;
use crate::{
    action::UiAction,
    audio::sound_trigger_ref::{CollectSoundTriggers, SoundTriggerSet},
    input::{osc::OSCHandler, osc::OscConfig, project_inputs::ProjectInputs},
    storage::asset::{Asset, project::Project, scene::Scene},
};

impl Engine {
    /// The project's own trigger inputs: tap, blackout and tempo.
    pub(super) fn evaluate_triggers(&mut self) {
        let Some(project) = &self.project else {
            self.blackout_hold = false;
            return;
        };
        if project.blackout_input_is_new() {
            self.blackout = !self.blackout;
        }
        self.blackout_hold = project.blackout_hold_input_is_live();
        if project.tap_input_is_new() {
            self.timing.tap(&self.persistent_state);
        }
        if project.double_input_is_new() {
            self.timing.multiply_speed(2.0, &self.persistent_state);
        }
        if project.half_input_is_new() {
            self.timing.multiply_speed(0.5, &self.persistent_state);
        }
    }

    /// Brings everything that follows project settings in line with the
    /// project, whichever way it changed: the patch's lamp layout and OSC.
    pub(super) fn apply_project_settings(&mut self) {
        // A new layout reaches the render thread as the snapshot's
        // `layout_generation`, which no dropped snapshot can lose.
        let patch = self.project.as_ref().map(|project| &project.patch);
        self.patch_layout.update(
            patch,
            &self.collections,
            self.collections.generation(),
            &mut self.output_config,
        );

        if let Some(project) = &self.project {
            self.applied_output.apply(project);
        }

        let osc_config = self.project.as_ref().map(|project| project.osc_config);
        if osc_config != self.osc_config {
            self.osc_config = osc_config;
            self.apply_osc_config(osc_config.unwrap_or_default());
        }
    }

    /// Puts `project` in place of the running one, or none. Its patch is
    /// derived on this tick (`apply_project_settings`).
    pub(super) fn replace_project(&mut self, project: Option<Project>) {
        self.project = project.map(Arc::new);
        self.patch_layout.patch_replaced();
    }

    /// Closes the running project and takes out everything it brought into the
    /// engine, so the engine is as it was before any project was loaded. The
    /// derived output (universes, colour orders, OSC, sound triggers) follows on
    /// this tick; the render thread drops its pipelines with the next snapshot.
    pub(super) fn unload_project(&mut self) {
        self.replace_project(None);
        self.project_id = None;
        self.selected_scene_instance = GridLocation::default();
        self.effect_selection.reset();
        self.controller_line = None;
        self.controllers.reset();
        self.output_config.routings = Default::default();
        self.set_hovered_routing(None);
        self.applied_output.unapply();
        self.external_control_state.reset();
        self.audio_pool.selected_device = None;
        self.audio_pool.restart_fft();
    }

    /// Puts `patch` in place of the running project's patch. It is derived on
    /// this tick (`apply_project_settings`) and the outputs follow.
    pub(super) fn replace_patch(&mut self, patch: Patch) {
        if let Some(project) = self.project.as_mut().map(Arc::make_mut) {
            project.patch = patch;
            self.patch_layout.patch_replaced();
        }
    }

    fn apply_osc_config(&mut self, config: OscConfig) {
        if let Some(handler) = self.osc_handler.take() {
            handler.stop();
        }
        if config.active {
            self.osc_handler = OSCHandler::start(config.port)
                .map_err(|err| {
                    UiAction::Error(format!(
                        "Could not start OSC on port {}: {err}",
                        config.port
                    ))
                    .enqueue();
                })
                .ok();
        }
    }

    /// Keeps the running sound triggers in step with the triggers the project
    /// and the scene assets use. Cheap when nothing changed.
    pub(super) fn sync_sound_triggers(&mut self) {
        let mut wanted = SoundTriggerSet::new();
        if let Some(project) = &self.project {
            project.collect_sound_triggers(&mut wanted);
        }
        for scene in Asset::<Scene>::all(&self.collections) {
            scene.data.collect_sound_triggers(&mut wanted);
        }
        if self.sound_data.sync_triggers(&wanted) {
            self.sound_data.save();
        }
    }
}