gled-model 2.31.0

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
use super::*;
use crate::patching::definition::{ColorOrder, Model, Photo};

fn pixel(name: &str, x_m: f32) -> Pixel {
    Pixel::new(name, [x_m, 0.0, 0.0], ColorOrder::Rgb)
}

/// Six pixels a..f, modes individual and "pairs" (ab cd ef).
fn six() -> FixtureDefinition {
    FixtureDefinition {
        pixels: ["a", "b", "c", "d", "e", "f"]
            .iter()
            .enumerate()
            .map(|(i, name)| pixel(name, i as f32 * 0.1))
            .collect(),
        modes: vec![
            Mode::individual(6),
            Mode {
                name: "pairs".to_string(),
                group_of: vec![0, 0, 1, 1, 2, 2],
            },
        ],
        ..Default::default()
    }
}

fn names(definition: &FixtureDefinition) -> Vec<&str> {
    definition.pixels.iter().map(|p| p.name.as_str()).collect()
}

fn set(indices: &[usize]) -> BTreeSet<usize> {
    indices.iter().copied().collect()
}

#[test]
fn normalize_relabels_groups_by_first_appearance() {
    let mut mode = Mode {
        name: "m".to_string(),
        group_of: vec![5, 5, 2, 7],
    };
    mode.normalize();
    assert_eq!(mode.group_of, vec![0, 0, 1, 2]);
}

#[test]
fn a_mode_with_a_gap_in_its_ids_is_no_partition() {
    let mode = Mode {
        name: "m".to_string(),
        group_of: vec![0, 2],
    };
    assert!(!mode.is_partition_of(2));
}

#[test]
fn inserted_pixels_get_their_own_groups() {
    let mut definition = six();
    let range = definition.insert_pixels(vec![pixel("g", 0.6), pixel("h", 0.7)]);
    assert_eq!(range, 6..8);
    assert_eq!(definition.modes[1].group_of, vec![0, 0, 1, 1, 2, 2, 3, 4]);
}

#[test]
fn removing_pixels_keeps_every_mode_a_partition() {
    let mut definition = six();
    definition.remove_pixels(&set(&[2, 3]));
    assert_eq!(names(&definition), ["a", "b", "e", "f"]);
    assert_eq!(definition.modes[0].group_of, vec![0, 1, 2, 3]);
    assert_eq!(definition.modes[1].group_of, vec![0, 0, 1, 1]);
}

#[test]
fn reordering_moves_the_dmx_offsets_with_the_pixels() {
    let mut definition = six();
    definition.pixels[5].set_color_order(ColorOrder::Grb);
    definition
        .reorder(&[5, 0, 1, 2, 3, 4])
        .expect("a permutation of six");
    assert_eq!(definition.pixels[0].color_order(), Some(ColorOrder::Grb));
    assert_eq!(definition.modes[1].group_of, vec![0, 1, 1, 2, 2, 0]);
}

#[test]
fn an_order_that_is_no_permutation_is_refused() {
    let mut definition = six();
    assert_eq!(
        definition.reorder(&[0, 0, 1, 2, 3, 4]),
        Err(DefinitionError::NotAPermutation { expected: 6 })
    );
}

#[test]
fn reversing_a_subset_leaves_the_others_in_place() {
    let mut definition = six();
    definition.reverse_pixels(&set(&[1, 2, 4]));
    assert_eq!(names(&definition), ["a", "e", "c", "d", "b", "f"]);
}

#[test]
fn moving_to_the_front_keeps_the_relative_order() {
    let mut definition = six();
    definition.move_to_front(&set(&[4, 1]));
    assert_eq!(names(&definition), ["b", "e", "a", "c", "d", "f"]);
}

#[test]
fn duplicates_are_appended_and_grouped_alone() {
    let mut definition = six();
    let copies = definition.duplicate_pixels(&set(&[0, 1]), [0.0, 0.5, 0.0]);
    assert_eq!(copies, 6..8);
    assert_eq!(definition.pixels[6].position_m, [0.0, 0.5, 0.0]);
    assert_eq!(definition.modes[1].group_of[6..], [3, 4]);
}

#[test]
fn grouping_then_ungrouping_leaves_a_partition() {
    let mut definition = six();
    let selection = set(&[1, 2]);
    definition
        .group_selection(1, &selection)
        .expect("pairs is editable");
    assert_eq!(definition.modes[1].group_of, vec![0, 1, 1, 2, 3, 3]);
    definition
        .ungroup_selection(1, &selection)
        .expect("pairs is editable");
    assert!(definition.modes[1].is_partition_of(6));
    assert_eq!(definition.modes[1].group_count(), 5);
}

#[test]
fn the_individual_mode_cannot_be_grouped() {
    let mut definition = six();
    assert_eq!(
        definition.group_selection(0, &set(&[0, 1])),
        Err(DefinitionError::IndividualMode)
    );
}

#[test]
fn the_individual_mode_cannot_be_removed() {
    let mut definition = six();
    assert_eq!(
        definition.remove_mode(0),
        Err(DefinitionError::IndividualMode)
    );
    assert_eq!(definition.remove_mode(1), Ok(()));
    assert_eq!(definition.modes.len(), 1);
}

#[test]
fn a_used_model_cannot_be_removed() {
    let mut definition = six();
    definition.models = vec![Model::circle("led", 0.01), Model::frame("body", [0.6, 0.1])];
    definition.outline = Some(1);
    assert_eq!(
        definition.remove_model(0),
        Err(DefinitionError::ModelInUse {
            name: "led".to_string(),
            pixels: 6
        })
    );
    assert_eq!(definition.remove_model(1), Ok(()));
    assert_eq!(definition.outline, None);
}

#[test]
fn fitting_the_size_covers_pixels_and_outline() {
    let mut definition = six();
    definition.models = vec![Model::circle("led", 0.02), Model::frame("body", [0.2, 0.3])];
    definition.outline = Some(1);
    definition.fit_size();
    // pixels reach from -0.01 to 0.51 m: half extent 0.51 on x
    assert!((definition.size_m[0] - 1.02).abs() < 1e-5);
    // the outline is taller than the pixels
    assert!((definition.size_m[1] - 0.3).abs() < 1e-5);
}

#[test]
fn centring_puts_the_pixels_around_the_origin() {
    let mut definition = six();
    definition.centre_pixels();
    assert!((definition.pixels[0].position_m[0] + 0.25).abs() < 1e-5);
    assert!((definition.pixels[5].position_m[0] - 0.25).abs() < 1e-5);
}

fn photo(offset_m: [f32; 2], size_m: [f32; 2]) -> Photo {
    Photo {
        file: "lamp.png".into(),
        size_m,
        offset_m,
        opacity: 0.1,
    }
}

#[test]
fn centring_the_pixels_keeps_the_photo_under_them() {
    let mut definition = six();
    definition.photo = Some(photo([0.3, 0.1], [0.8, 0.2]));
    definition.centre_pixels();
    // the pixels moved 0.25 m to the left, and so did the photo
    let photo = definition.photo.expect("a photo");
    assert!((photo.offset_m[0] - 0.05).abs() < 1e-5, "{photo:?}");
    assert!((photo.offset_m[1] - 0.1).abs() < 1e-5, "{photo:?}");
}

#[test]
fn fitting_the_size_to_the_photo_covers_it_on_both_sides() {
    let mut definition = six();
    definition.models = vec![Model::circle("led", 0.02)];
    definition.photo = Some(photo([0.0, 0.05], [0.4, 0.3]));
    definition.fit_size_to_photo();
    // the pixels are wider than the photo; the photo, 5 cm low, is taller
    assert!((definition.size_m[0] - 1.02).abs() < 1e-5);
    assert!((definition.size_m[1] - 0.4).abs() < 1e-5);
}

#[test]
fn without_a_photo_fitting_to_it_fits_the_pixels() {
    let mut fitted = six();
    fitted.fit_size();
    let mut to_photo = six();
    to_photo.fit_size_to_photo();
    assert_eq!(fitted.size_m, to_photo.size_m);
}

/// Six pixels whose "pairs" mode a hand-edited file left too short, and a
/// mode it left too long.
fn six_with_modes_that_do_not_fit() -> FixtureDefinition {
    let mut definition = six();
    definition.modes[1].group_of.truncate(3);
    definition.modes.push(Mode {
        name: "long".to_string(),
        group_of: vec![4, 4, 4, 9, 9, 9, 9, 9],
    });
    definition
}

/// Regression: removing or reordering pixels indexed every mode by pixel
/// and panicked on a mode shorter than the pixels.
#[test]
fn removing_pixels_fits_a_mode_read_too_short() {
    let mut definition = six_with_modes_that_do_not_fit();

    definition.remove_pixels(&set(&[0]));

    assert_eq!(definition.modes[1].group_of, [0, 1, 2, 3, 4]);
    assert_eq!(definition.modes[2].group_of, [0, 0, 1, 1, 1]);
}

#[test]
fn grouping_fits_a_mode_read_too_short() {
    let mut definition = six_with_modes_that_do_not_fit();

    definition
        .group_selection(1, &set(&[4, 5]))
        .expect("the mode exists");

    assert_eq!(definition.modes[1].group_of, [0, 0, 1, 2, 3, 3]);
}

#[test]
fn adding_pixels_fits_every_mode_first() {
    let mut definition = six_with_modes_that_do_not_fit();

    definition.insert_pixels(vec![pixel("g", 0.6)]);

    assert!(
        definition
            .modes
            .iter()
            .all(|mode| mode.is_partition_of(definition.pixels.len()))
    );
}