gled-model 2.31.1

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
use super::*;
use crate::asset_id::AssetId;
use crate::patching::topology::Port;
use crate::test_support::{collections_with, rgb_bar};

/// An asset collection holding `definition` only, and its id.
fn one_definition(definition: FixtureDefinition) -> (Collections, AssetId<FixtureDefinition>) {
    let (collections, ids) = collections_with(vec![definition]);
    (collections, ids[0])
}

/// An asset collection holding a single RGB pixel.
fn rgb_collections() -> (Collections, AssetId<FixtureDefinition>) {
    one_definition(rgb_bar(1, 0.05))
}

/// Adds a fixture already pointing at `definition`.
fn add(patch: &mut Patch, name: &str, definition: AssetId<FixtureDefinition>) -> FixtureId {
    patch.add_fixture(
        Fixture::with_definition(name, definition),
        LayoutPosition::default(),
    )
}

fn patch_with_one_fixture() -> (Patch, FixtureId, Collections) {
    let (collections, definition) = rgb_collections();
    let mut patch = Patch::default();
    let id = add(&mut patch, "a", definition);
    (patch, id, collections)
}

#[test]
fn addresses_follow_the_chain() {
    let (collections, definition) = rgb_collections();
    let mut patch = Patch::default();
    let ids: Vec<FixtureId> = (0..3)
        .map(|i| add(&mut patch, &format!("f{i}"), definition))
        .collect();
    patch.topology.make_successor(ids[0], ids[1]);
    patch.topology.make_successor(ids[1], ids[2]);

    let addresses = patch.addresses(&collections);

    // One RGB pixel: three channels each.
    assert_eq!(addresses[&ids[0]].into_pair(), (0, 0));
    assert_eq!(addresses[&ids[1]].into_pair(), (0, 3));
    assert_eq!(addresses[&ids[2]].into_pair(), (0, 6));
}

/// Regression: a chain wider than a universe runs into the next, but the
/// next chain was placed on the lowest universe no *port* named — which is
/// one an existing chain had already spilled into.
#[test]
fn a_new_chain_starts_past_a_chain_that_overflowed() {
    // 100 pixels: three of them no longer fit one universe
    let (collections, definition) = one_definition(rgb_bar(100, 1.0));
    let mut patch = Patch::default();
    let head = add(&mut patch, "head", definition);
    let tail = add(&mut patch, "tail", definition);
    patch.topology.make_successor(head, tail);

    // 300 channels each: the second one opens universe 1
    let addresses = patch.addresses(&collections);
    assert_eq!(addresses[&head].into_pair(), (0, 0));
    assert_eq!(addresses[&tail].into_pair(), (1, 0));

    assert_eq!(
        patch.occupied_universes(&collections, None),
        BTreeSet::from([0, 1])
    );
    assert_eq!(patch.lowest_free_universe(&collections, None), 2);
}

/// The chain being placed does not count as occupying anything, or it
/// would always be told to move.
#[test]
fn the_chain_being_placed_is_not_in_its_own_way() {
    let (collections, definition) = rgb_collections();
    let mut patch = Patch::default();
    let first = add(&mut patch, "first", definition);
    let second = add(&mut patch, "second", definition);

    assert_eq!(patch.lowest_free_universe(&collections, Some(second)), 1);
    assert_eq!(patch.lowest_free_universe(&collections, Some(first)), 0);
}

/// A gap left by a deleted chain is reused before the end.
#[test]
fn a_freed_universe_is_handed_out_again() {
    let (collections, definition) = rgb_collections();
    let mut patch = Patch::default();
    let first = add(&mut patch, "first", definition);
    add(&mut patch, "second", definition);
    patch.remove_fixture(first);

    assert_eq!(patch.lowest_free_universe(&collections, None), 0);
}

#[test]
fn a_fixture_never_straddles_a_universe_boundary() {
    let (collections, definition) = rgb_collections();
    let mut patch = Patch::default();
    // 171 three-channel fixtures: the 171st would start at channel 510 and
    // run past 512, so it must open the next universe.
    let ids: Vec<FixtureId> = (0..171)
        .map(|i| add(&mut patch, &format!("f{i}"), definition))
        .collect();
    for pair in ids.windows(2) {
        patch.topology.make_successor(pair[0], pair[1]);
    }

    let addresses = patch.addresses(&collections);

    assert_eq!(addresses[&ids[169]].into_pair(), (0, 507));
    assert_eq!(addresses[&ids[170]].into_pair(), (1, 0));
}

/// Universes past 127 used to get no address: an address packed its
/// universe into 7 bits.
#[test]
fn a_chain_on_a_high_universe_is_addressed() {
    // 300 channels each: the tail opens the universe after its head's
    let (collections, definition) = one_definition(rgb_bar(100, 1.0));
    let mut patch = Patch::default();
    let head = add(&mut patch, "head", definition);
    let tail = add(&mut patch, "tail", definition);
    patch.topology.make_successor(head, tail);
    patch.topology.ports.insert(head, Port::new(1000));

    let addresses = patch.addresses(&collections);

    assert_eq!(addresses[&head].into_pair(), (1000, 0));
    assert_eq!(addresses[&tail].into_pair(), (1001, 0));
}

/// Art-Net cannot address a universe past [`MAX_UNIVERSE`].
#[test]
fn a_fixture_past_the_highest_universe_gets_no_address() {
    // 300 channels each: the tail opens the universe after its head's
    let (collections, definition) = one_definition(rgb_bar(100, 1.0));
    let mut patch = Patch::default();
    let head = add(&mut patch, "head", definition);
    let tail = add(&mut patch, "tail", definition);
    patch.topology.make_successor(head, tail);
    let beyond = add(&mut patch, "beyond", definition);
    patch.topology.ports.insert(head, Port::new(MAX_UNIVERSE));
    patch.topology.ports.insert(beyond, Port::new(u16::MAX));

    let addresses = patch.addresses(&collections);

    assert_eq!(addresses[&head].into_pair(), (32767, 0));
    assert_eq!(addresses.get(&tail), None);
    assert_eq!(addresses.get(&beyond), None);
}

#[test]
fn separate_chains_start_on_their_own_universe() {
    let (collections, definition) = rgb_collections();
    let mut patch = Patch::default();
    let a = add(&mut patch, "a", definition);
    let b = add(&mut patch, "b", definition);

    let addresses = patch.addresses(&collections);

    assert_eq!(addresses[&a].into_pair(), (0, 0));
    assert_eq!(addresses[&b].into_pair(), (1, 0));
}

#[test]
fn default_layout_mode_falls_back_to_the_definition_default() {
    let (patch, id, collections) = patch_with_one_fixture();
    assert_eq!(
        patch.fixture_mode(LayoutRef::Topology, 0, id, &collections),
        Some(0)
    );
}

/// No built-in fallback: a fixture without a definition has no mode and no
/// address, rather than silently behaving like an RGB pixel.
#[test]
fn a_fixture_without_a_definition_resolves_to_nothing() {
    let (collections, _) = rgb_collections();
    let mut patch = Patch::default();
    let id = patch.add_fixture(Fixture::new("orphan"), LayoutPosition::default());

    assert!(patch.definition(id, &collections).is_none());
    assert_eq!(
        patch.fixture_mode(LayoutRef::Topology, 0, id, &collections),
        None
    );
    assert!(!patch.addresses(&collections).contains_key(&id));
}

/// An unknown channel width must not shift the rest of the chain.
#[test]
fn an_undefined_fixture_stops_addressing_its_chain() {
    let (collections, definition) = rgb_collections();
    let mut patch = Patch::default();
    let head = add(&mut patch, "head", definition);
    let orphan = patch.add_fixture(Fixture::new("orphan"), LayoutPosition::default());
    let tail = add(&mut patch, "tail", definition);
    patch.topology.make_successor(head, orphan);
    patch.topology.make_successor(orphan, tail);

    let addresses = patch.addresses(&collections);

    assert_eq!(addresses[&head].into_pair(), (0, 0));
    assert!(!addresses.contains_key(&orphan));
    assert!(!addresses.contains_key(&tail));
}

#[test]
fn a_user_mode_falls_back_for_unassigned_fixtures() {
    let (mut patch, id, collections) = patch_with_one_fixture();
    let layout = LayoutRef::Layout(patch.add_layout());
    patch.add_member(layout, id);
    let mode = patch
        .layout_mut(layout)
        .expect("the layout was just added")
        .add_mode();

    assert_eq!(patch.fixture_mode(layout, mode, id, &collections), Some(0));
}

#[test]
fn an_assigned_fixture_mode_wins() {
    let (mut patch, id, collections) = patch_with_one_fixture();
    // The fixture's definition has a single mode, so assigning mode 1 is
    // out of range and must fall back to the default.
    let layout = LayoutRef::Layout(patch.add_layout());
    patch.add_member(layout, id);
    let mode = patch
        .layout_mut(layout)
        .expect("the layout was just added")
        .add_mode();

    patch.set_fixture_mode(layout, mode, id, 1);
    assert_eq!(
        patch.fixture_mode(layout, mode, id, &collections),
        Some(0),
        "an index the definition does not have falls back to its default"
    );

    patch.set_fixture_mode(layout, mode, id, 0);
    assert_eq!(patch.fixture_mode(layout, mode, id, &collections), Some(0));
}

#[test]
fn assignments_are_per_layout_mode() {
    let (mut patch, id, collections) = patch_with_one_fixture();
    let layout = LayoutRef::Layout(patch.add_layout());
    patch.add_member(layout, id);
    let mode = patch
        .layout_mut(layout)
        .expect("the layout was just added")
        .add_mode();
    patch.set_fixture_mode(layout, mode, id, 0);

    assert!(
        patch
            .layout(layout)
            .expect("the layout was just added")
            .mode(0)
            .fixture_modes
            .is_empty(),
        "the default mode stores nothing"
    );
    assert_eq!(patch.fixture_mode(layout, 0, id, &collections), Some(0));
}

#[test]
fn layouts_are_overlapping_subsets_with_their_own_positions() {
    let (mut patch, id, _) = patch_with_one_fixture();
    let a = LayoutRef::Layout(patch.add_layout());
    let b = LayoutRef::Layout(patch.add_layout());
    patch.add_member(a, id);
    patch.add_member(b, id);

    patch
        .layout_mut(a)
        .expect("the layout was just added")
        .members
        .get_mut(&id)
        .expect("the fixture was just added to the layout")
        .pos
        .x = 500.0;

    assert_eq!(
        patch.layout(b).expect("the layout was just added").members[&id]
            .pos
            .x,
        100.0
    );
    assert_eq!(
        patch.topology.default_layout.members[&id].pos.x, 100.0,
        "moving a fixture in one layout leaves the others alone"
    );
}

#[test]
fn topology_membership_cannot_be_switched_off() {
    let (mut patch, id, _) = patch_with_one_fixture();
    patch.set_member(LayoutRef::Topology, id, false);
    assert!(patch.topology.default_layout.contains(id));
}

#[test]
fn setting_membership_adds_and_removes() {
    let (mut patch, id, _) = patch_with_one_fixture();
    let layout = LayoutRef::Layout(patch.add_layout());

    patch.set_member(layout, id, true);
    assert!(
        patch
            .layout(layout)
            .expect("the layout was just added")
            .contains(id)
    );

    patch.set_member(layout, id, false);
    assert!(
        !patch
            .layout(layout)
            .expect("the layout was just added")
            .contains(id)
    );
}

/// The patch is the serialized half of the editor, so everything the GUI
/// edits must survive a round trip.
#[test]
fn a_patch_survives_serialization() {
    let mut patch = Patch::default();
    let a = patch.add_fixture(
        Fixture::new("a"),
        LayoutPosition::new(emath::pos2(12.0, 34.0), 0.5, [1.5, 2.0]),
    );
    let b = patch.add_fixture(Fixture::new("b"), LayoutPosition::default());
    patch.topology.make_successor(a, b);
    patch.topology.default_layout.background = Some(std::path::PathBuf::from("/tmp/stage.png"));

    let layout = LayoutRef::Layout(patch.add_layout());
    patch.add_member(layout, a);
    patch
        .layout_mut(layout)
        .expect("the layout was just added")
        .name = "Truss".to_string();
    let mode = patch
        .layout_mut(layout)
        .expect("the layout was just added")
        .add_mode();
    patch.set_fixture_mode(layout, mode, a, 0);

    let json = serde_json::to_string(&patch).expect("serialize");
    let restored: Patch = serde_json::from_str(&json).expect("deserialize");

    assert_eq!(patch, restored);
    assert_eq!(restored.topology.chain_from(a), vec![a, b]);
    assert_eq!(
        restored
            .layout(layout)
            .expect("the layout was just added")
            .name,
        "Truss"
    );
    assert_eq!(
        restored
            .layout(layout)
            .expect("the layout was just added")
            .mode(mode)
            .fixture_modes[&a],
        0
    );
}

/// `Project` carries `patch` behind `#[serde(default)]`, so a project saved
/// before the patch existed must still load.
#[test]
fn a_missing_patch_deserializes_to_the_default() {
    let restored: Patch = serde_json::from_str("{}").expect("deserialize");
    assert_eq!(restored, Patch::default());
    assert!(restored.topology.fixtures.is_empty());
    assert!(restored.layouts.is_empty());
}

#[test]
fn a_definition_with_more_channels_shifts_the_chain() {
    let (collections, definition) = one_definition(rgb_bar(2, 0.1));
    let mut patch = Patch::default();
    let a = add(&mut patch, "a", definition);
    let b = add(&mut patch, "b", definition);
    patch.topology.make_successor(a, b);

    let addresses = patch.addresses(&collections);

    assert_eq!(addresses[&a].into_pair(), (0, 0));
    assert_eq!(addresses[&b].into_pair(), (0, 6));
}