gled-model 2.31.1

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
//! Editing a definition without breaking it: every change to the pixels is
//! carried into every mode, so each mode stays a partition of the pixels.

use crate::patching::definition::{FixtureDefinition, Mode, Pixel, group_ids};
use std::collections::BTreeSet;
use std::ops::Range;

/// Why a definition edit was refused.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DefinitionError {
    #[error("the new order is not a permutation of the {expected} pixels")]
    NotAPermutation { expected: usize },
    #[error("there is no mode {0}")]
    NoSuchMode(usize),
    #[error("the first mode, \"individual\", gives every pixel its own group and cannot change")]
    IndividualMode,
    #[error("there is no model {0}")]
    NoSuchModel(usize),
    #[error("\"{name}\" still draws {pixels} pixels; give them another model first")]
    ModelInUse { name: String, pixels: usize },
}

impl Mode {
    /// Relabels the groups by first appearance, so the ids run `0..n` in
    /// pixel order. Two modes are the same partition exactly when their
    /// normalized `group_of` are equal.
    pub(crate) fn normalize(&mut self) {
        self.group_of = group_ids(self.group_of.iter().copied());
    }

    /// Whether the mode assigns each of `pixel_count` pixels a group, with
    /// group ids `0..n` and none left out.
    pub fn is_partition_of(&self, pixel_count: usize) -> bool {
        let ids: BTreeSet<usize> = self.group_of.iter().copied().collect();
        self.group_of.len() == pixel_count && ids.iter().copied().eq(0..ids.len())
    }

    /// Makes the mode a partition of `pixel_count` pixels again, should a
    /// file have it cover more or fewer: a pixel it misses gets a group of
    /// its own, the groups of pixels that are not there go.
    fn fit(&mut self, pixel_count: usize) {
        self.group_of.truncate(pixel_count);
        self.extend_individual(pixel_count - self.group_of.len());
        self.normalize();
    }

    /// Gives `added` new pixels at the end a group of their own each.
    fn extend_individual(&mut self, added: usize) {
        let next = self.group_of.iter().max().map_or(0, |max| max + 1);
        self.group_of.extend(next..next + added);
    }

    /// Takes the pixels' groups along a new pixel order: new pixel `i` is old
    /// pixel `order[i]`.
    fn remap(&mut self, order: &[usize]) {
        self.group_of = order.iter().map(|old| self.group_of[*old]).collect();
        self.normalize();
    }
}

impl FixtureDefinition {
    /// Appends pixels; each gets a group of its own in every mode. Returns
    /// where they landed.
    pub fn insert_pixels(&mut self, pixels: Vec<Pixel>) -> Range<usize> {
        self.fit_modes();
        let start = self.pixels.len();
        let added = pixels.len();
        self.pixels.extend(pixels);
        for mode in &mut self.modes {
            mode.extend_individual(added);
        }
        self.debug_check();
        start..start + added
    }

    pub fn remove_pixels(&mut self, indices: &BTreeSet<usize>) {
        let keep: Vec<usize> = (0..self.pixels.len())
            .filter(|index| !indices.contains(index))
            .collect();
        self.apply_order(&keep);
    }

    /// Rearranges the pixels, and with them the DMX order: new pixel `i` is
    /// old pixel `order[i]`.
    pub fn reorder(&mut self, order: &[usize]) -> Result<(), DefinitionError> {
        let unique: BTreeSet<usize> = order.iter().copied().collect();
        if order.len() != self.pixels.len() || !unique.iter().copied().eq(0..self.pixels.len()) {
            return Err(DefinitionError::NotAPermutation {
                expected: self.pixels.len(),
            });
        }
        self.apply_order(order);
        Ok(())
    }

    /// Reverses the DMX order of the selected pixels among the slots they
    /// occupy; every other pixel keeps its place.
    pub fn reverse_pixels(&mut self, indices: &BTreeSet<usize>) {
        let slots: Vec<usize> = self.existing(indices).collect();
        let mut order: Vec<usize> = (0..self.pixels.len()).collect();
        for (slot, source) in slots.iter().zip(slots.iter().rev()) {
            order[*slot] = *source;
        }
        self.apply_order(&order);
    }

    /// Makes the selected pixels the first ones in DMX order, keeping their
    /// relative order, the rest following as before.
    pub fn move_to_front(&mut self, indices: &BTreeSet<usize>) {
        let selected: Vec<usize> = self.existing(indices).collect();
        let order: Vec<usize> = selected
            .iter()
            .copied()
            .chain((0..self.pixels.len()).filter(|index| !indices.contains(index)))
            .collect();
        self.apply_order(&order);
    }

    /// Appends a copy of every selected pixel, moved by `offset_m`. Returns
    /// where the copies landed.
    pub fn duplicate_pixels(
        &mut self,
        indices: &BTreeSet<usize>,
        offset_m: [f32; 3],
    ) -> Range<usize> {
        let copies = self
            .existing(indices)
            .map(|index| {
                let mut copy = self.pixels[index].clone();
                copy.name = format!("{} copy", copy.name);
                for (axis, offset) in copy.position_m.iter_mut().zip(offset_m) {
                    *axis += offset;
                }
                copy
            })
            .collect();
        self.insert_pixels(copies)
    }

    pub fn set_model_of(
        &mut self,
        indices: &BTreeSet<usize>,
        model: usize,
    ) -> Result<(), DefinitionError> {
        if model >= self.models.len() {
            return Err(DefinitionError::NoSuchModel(model));
        }
        for index in self.existing(indices).collect::<Vec<_>>() {
            self.pixels[index].model = model;
        }
        Ok(())
    }

    /// Removes a model no pixel draws with.
    pub fn remove_model(&mut self, model: usize) -> Result<(), DefinitionError> {
        if model >= self.models.len() {
            return Err(DefinitionError::NoSuchModel(model));
        }
        let users = self
            .pixels
            .iter()
            .filter(|pixel| pixel.model == model)
            .count();
        if users > 0 {
            return Err(DefinitionError::ModelInUse {
                name: self.models[model].name.clone(),
                pixels: users,
            });
        }
        self.models.remove(model);
        for pixel in &mut self.pixels {
            if pixel.model > model {
                pixel.model -= 1;
            }
        }
        self.outline = match self.outline {
            Some(outline) if outline == model => None,
            Some(outline) if outline > model => Some(outline - 1),
            outline => outline,
        };
        Ok(())
    }

    /// The smallest rectangle around every pixel's drawing, in metres, as
    /// `(min, max)`. A pixel whose model is missing counts as a point.
    pub fn pixel_bounds_m(&self) -> Option<([f32; 2], [f32; 2])> {
        let mut bounds: Option<([f32; 2], [f32; 2])> = None;
        for pixel in &self.pixels {
            let half = self.model(pixel.model).map_or([0.0; 2], |model| {
                [model.size_m[0] / 2.0, model.size_m[1] / 2.0]
            });
            let (sin, cos) = pixel.rotation.sin_cos();
            // half extent of the rotated rectangle along each axis
            let extent = [
                (half[0] * cos).abs() + (half[1] * sin).abs(),
                (half[0] * sin).abs() + (half[1] * cos).abs(),
            ];
            let [x, y, _] = pixel.position_m;
            let low = [x - extent[0], y - extent[1]];
            let high = [x + extent[0], y + extent[1]];
            bounds = Some(match bounds {
                None => (low, high),
                Some((min, max)) => (
                    [min[0].min(low[0]), min[1].min(low[1])],
                    [max[0].max(high[0]), max[1].max(high[1])],
                ),
            });
        }
        bounds
    }

    /// Sizes the fixture to cover its pixels and its outline. The footprint
    /// is centred on the fixture's origin, as a layout draws it, so pixels off
    /// centre widen it on both sides; the depth is kept.
    pub fn fit_size(&mut self) {
        let pixels = self.pixel_bounds_m().map_or([0.0; 2], |(min, max)| {
            [
                min[0].abs().max(max[0].abs()),
                min[1].abs().max(max[1].abs()),
            ]
        });
        let outline = self
            .outline
            .and_then(|outline| self.model(outline))
            .map_or([0.0; 2], |outline| outline.size_m.map(|size| size / 2.0));
        self.size_m[0] = 2.0 * pixels[0].max(outline[0]);
        self.size_m[1] = 2.0 * pixels[1].max(outline[1]);
    }

    /// Like [`FixtureDefinition::fit_size`], and covering the photo too, so
    /// the fixture's frame (what a layout selects and outlines) holds the
    /// whole picture of the lamp.
    pub fn fit_size_to_photo(&mut self) {
        self.fit_size();
        if let Some(photo) = &self.photo {
            for (size, reach) in self.size_m.iter_mut().zip(photo.reach_m()) {
                *size = size.max(2.0 * reach);
            }
        }
    }

    /// Moves every pixel so their drawings are centred on the fixture's
    /// origin, which is where a layout places and rotates the fixture. The
    /// photo moves along, so it stays under its LEDs.
    pub fn centre_pixels(&mut self) {
        let Some((min, max)) = self.pixel_bounds_m() else {
            return;
        };
        let centre = [(min[0] + max[0]) / 2.0, (min[1] + max[1]) / 2.0];
        for pixel in &mut self.pixels {
            pixel.position_m[0] -= centre[0];
            pixel.position_m[1] -= centre[1];
        }
        if let Some(photo) = &mut self.photo {
            photo.offset_m[0] -= centre[0];
            photo.offset_m[1] -= centre[1];
        }
    }

    /// Adds a mode in which every pixel has its own group, for the user to
    /// group from there. Returns its index.
    pub fn add_mode(&mut self, name: impl Into<String>) -> usize {
        let mut mode = Mode::individual(self.pixels.len());
        mode.name = name.into();
        self.modes.push(mode);
        self.modes.len() - 1
    }

    /// Removes a mode. Layouts refer to a definition's modes by index, so a
    /// fixture that used a later mode moves to its neighbour or falls back to
    /// "individual".
    pub fn remove_mode(&mut self, index: usize) -> Result<(), DefinitionError> {
        self.editable_mode(index)?;
        self.modes.remove(index);
        Ok(())
    }

    /// Puts the selected pixels into one group of `mode`.
    pub fn group_selection(
        &mut self,
        mode: usize,
        pixels: &BTreeSet<usize>,
    ) -> Result<(), DefinitionError> {
        let selected: Vec<usize> = self.existing(pixels).collect();
        let mode = self.editable_mode(mode)?;
        // a label no group has yet; normalize turns it into a proper id
        let joined = mode.group_of.iter().max().map_or(0, |max| max + 1);
        for pixel in selected {
            mode.group_of[pixel] = joined;
        }
        mode.normalize();
        Ok(())
    }

    /// Gives every selected pixel a group of its own in `mode`.
    pub fn ungroup_selection(
        &mut self,
        mode: usize,
        pixels: &BTreeSet<usize>,
    ) -> Result<(), DefinitionError> {
        let selected: Vec<usize> = self.existing(pixels).collect();
        let mode = self.editable_mode(mode)?;
        let next = mode.group_of.iter().max().map_or(0, |max| max + 1);
        for (offset, pixel) in selected.into_iter().enumerate() {
            mode.group_of[pixel] = next + offset;
        }
        mode.normalize();
        Ok(())
    }

    /// Gives every pixel a group of its own in `mode`, keeping its name.
    pub fn each_own_group(&mut self, mode: usize) -> Result<(), DefinitionError> {
        let pixel_count = self.pixels.len();
        let mode = self.editable_mode(mode)?;
        mode.group_of = (0..pixel_count).collect();
        Ok(())
    }

    /// The number the next generated pixel is named with.
    pub fn next_pixel_number(&self) -> usize {
        self.pixels.len()
    }

    /// The mode at `index`, unless it is missing or "individual".
    fn editable_mode(&mut self, index: usize) -> Result<&mut Mode, DefinitionError> {
        if index == self.default_mode() {
            return Err(DefinitionError::IndividualMode);
        }
        self.fit_modes();
        self.modes
            .get_mut(index)
            .ok_or(DefinitionError::NoSuchMode(index))
    }

    /// The selected indices that name an existing pixel, in pixel order.
    fn existing<'a>(&self, indices: &'a BTreeSet<usize>) -> impl Iterator<Item = usize> + 'a {
        let pixel_count = self.pixels.len();
        indices
            .iter()
            .copied()
            .filter(move |index| *index < pixel_count)
    }

    /// Keeps the pixels `order` names, in that order, and every mode with
    /// them. `order` holds each index at most once.
    fn apply_order(&mut self, order: &[usize]) {
        self.fit_modes();
        self.pixels = order.iter().map(|old| self.pixels[*old].clone()).collect();
        for mode in &mut self.modes {
            mode.remap(order);
        }
        self.debug_check();
    }

    /// Every mode fitted to the pixels (see [`Mode::fit`]), so an edit of a
    /// definition read from a file cannot index past a mode's end.
    fn fit_modes(&mut self) {
        let pixel_count = self.pixels.len();
        for mode in &mut self.modes {
            mode.fit(pixel_count);
        }
    }

    fn debug_check(&self) {
        debug_assert!(
            self.modes
                .iter()
                .all(|mode| mode.is_partition_of(self.pixels.len())),
            "every mode must stay a partition of the pixels"
        );
    }
}

#[cfg(test)]
mod tests;