gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The patch's chains — strings of lamps on one output — as the canvas
//! draws them: in wiring order, and each in a colour of its own.

use crate::canvas::definition_drawing::mode_color;
use crate::fixture_editor::state::{CanvasMode, EditorState};
use egui::Color32;
use gled_model::collection::Collections;
use gled_model::patching::Patch;
use gled_model::patching::fixture::FixtureId;
use gled_model::patching::layout::LayoutRef;
use std::collections::BTreeMap;

/// Colour a chain, a string of lamps on one output, so neighbouring strings
/// can be told apart. Hues lie a golden angle apart: however many chains
/// there are, consecutive ones differ most.
pub(crate) fn chain_color(chain: usize) -> Color32 {
    let hue = (chain as f32 * 0.618_034).fract();
    egui::ecolor::Hsva::new(hue, 0.65, 1.0, 1.0).into()
}

/// Draw order: whole chains, so an edge can join consecutive fixtures.
pub(super) fn draw_order(patch: &Patch, mode: CanvasMode, drawn: LayoutRef) -> Vec<Vec<FixtureId>> {
    let chains = patch
        .topology
        .chain_heads()
        .into_iter()
        .map(|head| patch.topology.chain_from(head));
    if mode == CanvasMode::Topology {
        return chains.collect();
    }
    // A layout holds only its own fixtures, so a chain enters it as the runs
    // of neighbours it contains: an edge to a fixture that is not here has no
    // end to point at.
    let Some(layout) = patch.layout(drawn) else {
        return vec![];
    };
    chains
        .flat_map(|chain| {
            let mut runs = vec![];
            let mut run = vec![];
            for id in chain {
                if layout.contains(id) {
                    run.push(id);
                } else if !run.is_empty() {
                    runs.push(std::mem::take(&mut run));
                }
            }
            if !run.is_empty() {
                runs.push(run);
            }
            runs
        })
        .collect()
}

/// The colour each drawn fixture is drawn in. Wiring strings in the topology,
/// each string has a colour of its own; anywhere else, or with a mode picked,
/// the mode a fixture resolves to.
pub(super) fn fixture_colors(
    patch: &Patch,
    state: &EditorState,
    chains: &[Vec<FixtureId>],
    mode: CanvasMode,
    collections: &Collections,
) -> BTreeMap<FixtureId, Color32> {
    let by_chain = mode == CanvasMode::Topology && state.selected_mode == 0;
    let chain_index: BTreeMap<FixtureId, usize> = patch
        .topology
        .chain_heads()
        .into_iter()
        .enumerate()
        .map(|(index, head)| (head, index))
        .collect();
    chains
        .iter()
        .flatten()
        .map(|id| {
            let resolved =
                patch.fixture_mode(state.selection, state.selected_mode, *id, collections);
            let chain = chain_index.get(&patch.topology.chain_head_of(*id));
            let color = match (by_chain, resolved, chain) {
                (true, Some(_), Some(chain)) => chain_color(*chain),
                _ => mode_color(resolved),
            };
            (*id, color)
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use gled_model::patching::fixture::{Fixture, LayoutPosition};

    #[test]
    fn draw_order_walks_chains_in_the_topology() {
        let mut patch = Patch::default();
        let a = patch.add_fixture(Fixture::new("a"), LayoutPosition::default());
        let b = patch.add_fixture(Fixture::new("b"), LayoutPosition::default());
        patch.topology.make_successor(a, b);

        let order = draw_order(&patch, CanvasMode::Topology, LayoutRef::Topology);

        assert_eq!(order, vec![vec![a, b]]);
    }

    /// A layout shows what it holds and nothing else, so a fixture it leaves
    /// out is absent, and so is the edge that would have pointed at it.
    #[test]
    fn a_layout_leaves_out_what_it_does_not_hold() {
        let mut patch = Patch::default();
        let a = patch.add_fixture(Fixture::new("a"), LayoutPosition::default());
        let b = patch.add_fixture(Fixture::new("b"), LayoutPosition::default());
        patch.topology.make_successor(a, b);
        let layout = LayoutRef::Layout(patch.add_layout());
        patch.add_member(layout, a);

        let order = draw_order(&patch, CanvasMode::Layout, layout);

        assert_eq!(order, vec![vec![a]]);
    }

    /// A gap in a chain cuts it: the fixtures on either side are still drawn,
    /// but as separate runs, so no edge spans the fixture that is missing.
    #[test]
    fn a_chain_is_cut_where_a_layout_skips_a_fixture() {
        let mut patch = Patch::default();
        let a = patch.add_fixture(Fixture::new("a"), LayoutPosition::default());
        let b = patch.add_fixture(Fixture::new("b"), LayoutPosition::default());
        let c = patch.add_fixture(Fixture::new("c"), LayoutPosition::default());
        patch.topology.make_successor(a, b);
        patch.topology.make_successor(b, c);
        let layout = LayoutRef::Layout(patch.add_layout());
        patch.add_member(layout, a);
        patch.add_member(layout, c);

        let order = draw_order(&patch, CanvasMode::Layout, layout);

        assert_eq!(order, vec![vec![a], vec![c]]);
    }

    /// Strings next to each other in the list must not look alike.
    #[test]
    fn neighbouring_chains_get_clearly_different_colours() {
        let distance = |a: Color32, b: Color32| {
            (a.r() as i32 - b.r() as i32).abs()
                + (a.g() as i32 - b.g() as i32).abs()
                + (a.b() as i32 - b.b() as i32).abs()
        };
        for chain in 0..40 {
            assert!(
                distance(chain_color(chain), chain_color(chain + 1)) > 60,
                "chain {chain}"
            );
        }
    }
}