Skip to main content

gled_model/patching/definition/
edit.rs

1//! Editing a definition without breaking it: every change to the pixels is
2//! carried into every mode, so each mode stays a partition of the pixels.
3
4use crate::patching::definition::{FixtureDefinition, Mode, Pixel, group_ids};
5use std::collections::BTreeSet;
6use std::ops::Range;
7
8/// Why a definition edit was refused.
9#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
10pub enum DefinitionError {
11    #[error("the new order is not a permutation of the {expected} pixels")]
12    NotAPermutation { expected: usize },
13    #[error("there is no mode {0}")]
14    NoSuchMode(usize),
15    #[error("the first mode, \"individual\", gives every pixel its own group and cannot change")]
16    IndividualMode,
17    #[error("there is no model {0}")]
18    NoSuchModel(usize),
19    #[error("\"{name}\" still draws {pixels} pixels; give them another model first")]
20    ModelInUse { name: String, pixels: usize },
21}
22
23impl Mode {
24    /// Relabels the groups by first appearance, so the ids run `0..n` in
25    /// pixel order. Two modes are the same partition exactly when their
26    /// normalized `group_of` are equal.
27    pub(crate) fn normalize(&mut self) {
28        self.group_of = group_ids(self.group_of.iter().copied());
29    }
30
31    /// Whether the mode assigns each of `pixel_count` pixels a group, with
32    /// group ids `0..n` and none left out.
33    pub fn is_partition_of(&self, pixel_count: usize) -> bool {
34        let ids: BTreeSet<usize> = self.group_of.iter().copied().collect();
35        self.group_of.len() == pixel_count && ids.iter().copied().eq(0..ids.len())
36    }
37
38    /// Makes the mode a partition of `pixel_count` pixels again, should a
39    /// file have it cover more or fewer: a pixel it misses gets a group of
40    /// its own, the groups of pixels that are not there go.
41    fn fit(&mut self, pixel_count: usize) {
42        self.group_of.truncate(pixel_count);
43        self.extend_individual(pixel_count - self.group_of.len());
44        self.normalize();
45    }
46
47    /// Gives `added` new pixels at the end a group of their own each.
48    fn extend_individual(&mut self, added: usize) {
49        let next = self.group_of.iter().max().map_or(0, |max| max + 1);
50        self.group_of.extend(next..next + added);
51    }
52
53    /// Takes the pixels' groups along a new pixel order: new pixel `i` is old
54    /// pixel `order[i]`.
55    fn remap(&mut self, order: &[usize]) {
56        self.group_of = order.iter().map(|old| self.group_of[*old]).collect();
57        self.normalize();
58    }
59}
60
61impl FixtureDefinition {
62    /// Appends pixels; each gets a group of its own in every mode. Returns
63    /// where they landed.
64    pub fn insert_pixels(&mut self, pixels: Vec<Pixel>) -> Range<usize> {
65        self.fit_modes();
66        let start = self.pixels.len();
67        let added = pixels.len();
68        self.pixels.extend(pixels);
69        for mode in &mut self.modes {
70            mode.extend_individual(added);
71        }
72        self.debug_check();
73        start..start + added
74    }
75
76    pub fn remove_pixels(&mut self, indices: &BTreeSet<usize>) {
77        let keep: Vec<usize> = (0..self.pixels.len())
78            .filter(|index| !indices.contains(index))
79            .collect();
80        self.apply_order(&keep);
81    }
82
83    /// Rearranges the pixels, and with them the DMX order: new pixel `i` is
84    /// old pixel `order[i]`.
85    pub fn reorder(&mut self, order: &[usize]) -> Result<(), DefinitionError> {
86        let unique: BTreeSet<usize> = order.iter().copied().collect();
87        if order.len() != self.pixels.len() || !unique.iter().copied().eq(0..self.pixels.len()) {
88            return Err(DefinitionError::NotAPermutation {
89                expected: self.pixels.len(),
90            });
91        }
92        self.apply_order(order);
93        Ok(())
94    }
95
96    /// Reverses the DMX order of the selected pixels among the slots they
97    /// occupy; every other pixel keeps its place.
98    pub fn reverse_pixels(&mut self, indices: &BTreeSet<usize>) {
99        let slots: Vec<usize> = self.existing(indices).collect();
100        let mut order: Vec<usize> = (0..self.pixels.len()).collect();
101        for (slot, source) in slots.iter().zip(slots.iter().rev()) {
102            order[*slot] = *source;
103        }
104        self.apply_order(&order);
105    }
106
107    /// Makes the selected pixels the first ones in DMX order, keeping their
108    /// relative order, the rest following as before.
109    pub fn move_to_front(&mut self, indices: &BTreeSet<usize>) {
110        let selected: Vec<usize> = self.existing(indices).collect();
111        let order: Vec<usize> = selected
112            .iter()
113            .copied()
114            .chain((0..self.pixels.len()).filter(|index| !indices.contains(index)))
115            .collect();
116        self.apply_order(&order);
117    }
118
119    /// Appends a copy of every selected pixel, moved by `offset_m`. Returns
120    /// where the copies landed.
121    pub fn duplicate_pixels(
122        &mut self,
123        indices: &BTreeSet<usize>,
124        offset_m: [f32; 3],
125    ) -> Range<usize> {
126        let copies = self
127            .existing(indices)
128            .map(|index| {
129                let mut copy = self.pixels[index].clone();
130                copy.name = format!("{} copy", copy.name);
131                for (axis, offset) in copy.position_m.iter_mut().zip(offset_m) {
132                    *axis += offset;
133                }
134                copy
135            })
136            .collect();
137        self.insert_pixels(copies)
138    }
139
140    pub fn set_model_of(
141        &mut self,
142        indices: &BTreeSet<usize>,
143        model: usize,
144    ) -> Result<(), DefinitionError> {
145        if model >= self.models.len() {
146            return Err(DefinitionError::NoSuchModel(model));
147        }
148        for index in self.existing(indices).collect::<Vec<_>>() {
149            self.pixels[index].model = model;
150        }
151        Ok(())
152    }
153
154    /// Removes a model no pixel draws with.
155    pub fn remove_model(&mut self, model: usize) -> Result<(), DefinitionError> {
156        if model >= self.models.len() {
157            return Err(DefinitionError::NoSuchModel(model));
158        }
159        let users = self
160            .pixels
161            .iter()
162            .filter(|pixel| pixel.model == model)
163            .count();
164        if users > 0 {
165            return Err(DefinitionError::ModelInUse {
166                name: self.models[model].name.clone(),
167                pixels: users,
168            });
169        }
170        self.models.remove(model);
171        for pixel in &mut self.pixels {
172            if pixel.model > model {
173                pixel.model -= 1;
174            }
175        }
176        self.outline = match self.outline {
177            Some(outline) if outline == model => None,
178            Some(outline) if outline > model => Some(outline - 1),
179            outline => outline,
180        };
181        Ok(())
182    }
183
184    /// The smallest rectangle around every pixel's drawing, in metres, as
185    /// `(min, max)`. A pixel whose model is missing counts as a point.
186    pub fn pixel_bounds_m(&self) -> Option<([f32; 2], [f32; 2])> {
187        let mut bounds: Option<([f32; 2], [f32; 2])> = None;
188        for pixel in &self.pixels {
189            let half = self.model(pixel.model).map_or([0.0; 2], |model| {
190                [model.size_m[0] / 2.0, model.size_m[1] / 2.0]
191            });
192            let (sin, cos) = pixel.rotation.sin_cos();
193            // half extent of the rotated rectangle along each axis
194            let extent = [
195                (half[0] * cos).abs() + (half[1] * sin).abs(),
196                (half[0] * sin).abs() + (half[1] * cos).abs(),
197            ];
198            let [x, y, _] = pixel.position_m;
199            let low = [x - extent[0], y - extent[1]];
200            let high = [x + extent[0], y + extent[1]];
201            bounds = Some(match bounds {
202                None => (low, high),
203                Some((min, max)) => (
204                    [min[0].min(low[0]), min[1].min(low[1])],
205                    [max[0].max(high[0]), max[1].max(high[1])],
206                ),
207            });
208        }
209        bounds
210    }
211
212    /// Sizes the fixture to cover its pixels and its outline. The footprint
213    /// is centred on the fixture's origin, as a layout draws it, so pixels off
214    /// centre widen it on both sides; the depth is kept.
215    pub fn fit_size(&mut self) {
216        let pixels = self.pixel_bounds_m().map_or([0.0; 2], |(min, max)| {
217            [
218                min[0].abs().max(max[0].abs()),
219                min[1].abs().max(max[1].abs()),
220            ]
221        });
222        let outline = self
223            .outline
224            .and_then(|outline| self.model(outline))
225            .map_or([0.0; 2], |outline| outline.size_m.map(|size| size / 2.0));
226        self.size_m[0] = 2.0 * pixels[0].max(outline[0]);
227        self.size_m[1] = 2.0 * pixels[1].max(outline[1]);
228    }
229
230    /// Like [`FixtureDefinition::fit_size`], and covering the photo too, so
231    /// the fixture's frame (what a layout selects and outlines) holds the
232    /// whole picture of the lamp.
233    pub fn fit_size_to_photo(&mut self) {
234        self.fit_size();
235        if let Some(photo) = &self.photo {
236            for (size, reach) in self.size_m.iter_mut().zip(photo.reach_m()) {
237                *size = size.max(2.0 * reach);
238            }
239        }
240    }
241
242    /// Moves every pixel so their drawings are centred on the fixture's
243    /// origin, which is where a layout places and rotates the fixture. The
244    /// photo moves along, so it stays under its LEDs.
245    pub fn centre_pixels(&mut self) {
246        let Some((min, max)) = self.pixel_bounds_m() else {
247            return;
248        };
249        let centre = [(min[0] + max[0]) / 2.0, (min[1] + max[1]) / 2.0];
250        for pixel in &mut self.pixels {
251            pixel.position_m[0] -= centre[0];
252            pixel.position_m[1] -= centre[1];
253        }
254        if let Some(photo) = &mut self.photo {
255            photo.offset_m[0] -= centre[0];
256            photo.offset_m[1] -= centre[1];
257        }
258    }
259
260    /// Adds a mode in which every pixel has its own group, for the user to
261    /// group from there. Returns its index.
262    pub fn add_mode(&mut self, name: impl Into<String>) -> usize {
263        let mut mode = Mode::individual(self.pixels.len());
264        mode.name = name.into();
265        self.modes.push(mode);
266        self.modes.len() - 1
267    }
268
269    /// Removes a mode. Layouts refer to a definition's modes by index, so a
270    /// fixture that used a later mode moves to its neighbour or falls back to
271    /// "individual".
272    pub fn remove_mode(&mut self, index: usize) -> Result<(), DefinitionError> {
273        self.editable_mode(index)?;
274        self.modes.remove(index);
275        Ok(())
276    }
277
278    /// Puts the selected pixels into one group of `mode`.
279    pub fn group_selection(
280        &mut self,
281        mode: usize,
282        pixels: &BTreeSet<usize>,
283    ) -> Result<(), DefinitionError> {
284        let selected: Vec<usize> = self.existing(pixels).collect();
285        let mode = self.editable_mode(mode)?;
286        // a label no group has yet; normalize turns it into a proper id
287        let joined = mode.group_of.iter().max().map_or(0, |max| max + 1);
288        for pixel in selected {
289            mode.group_of[pixel] = joined;
290        }
291        mode.normalize();
292        Ok(())
293    }
294
295    /// Gives every selected pixel a group of its own in `mode`.
296    pub fn ungroup_selection(
297        &mut self,
298        mode: usize,
299        pixels: &BTreeSet<usize>,
300    ) -> Result<(), DefinitionError> {
301        let selected: Vec<usize> = self.existing(pixels).collect();
302        let mode = self.editable_mode(mode)?;
303        let next = mode.group_of.iter().max().map_or(0, |max| max + 1);
304        for (offset, pixel) in selected.into_iter().enumerate() {
305            mode.group_of[pixel] = next + offset;
306        }
307        mode.normalize();
308        Ok(())
309    }
310
311    /// Gives every pixel a group of its own in `mode`, keeping its name.
312    pub fn each_own_group(&mut self, mode: usize) -> Result<(), DefinitionError> {
313        let pixel_count = self.pixels.len();
314        let mode = self.editable_mode(mode)?;
315        mode.group_of = (0..pixel_count).collect();
316        Ok(())
317    }
318
319    /// The number the next generated pixel is named with.
320    pub fn next_pixel_number(&self) -> usize {
321        self.pixels.len()
322    }
323
324    /// The mode at `index`, unless it is missing or "individual".
325    fn editable_mode(&mut self, index: usize) -> Result<&mut Mode, DefinitionError> {
326        if index == self.default_mode() {
327            return Err(DefinitionError::IndividualMode);
328        }
329        self.fit_modes();
330        self.modes
331            .get_mut(index)
332            .ok_or(DefinitionError::NoSuchMode(index))
333    }
334
335    /// The selected indices that name an existing pixel, in pixel order.
336    fn existing<'a>(&self, indices: &'a BTreeSet<usize>) -> impl Iterator<Item = usize> + 'a {
337        let pixel_count = self.pixels.len();
338        indices
339            .iter()
340            .copied()
341            .filter(move |index| *index < pixel_count)
342    }
343
344    /// Keeps the pixels `order` names, in that order, and every mode with
345    /// them. `order` holds each index at most once.
346    fn apply_order(&mut self, order: &[usize]) {
347        self.fit_modes();
348        self.pixels = order.iter().map(|old| self.pixels[*old].clone()).collect();
349        for mode in &mut self.modes {
350            mode.remap(order);
351        }
352        self.debug_check();
353    }
354
355    /// Every mode fitted to the pixels (see [`Mode::fit`]), so an edit of a
356    /// definition read from a file cannot index past a mode's end.
357    fn fit_modes(&mut self) {
358        let pixel_count = self.pixels.len();
359        for mode in &mut self.modes {
360            mode.fit(pixel_count);
361        }
362    }
363
364    fn debug_check(&self) {
365        debug_assert!(
366            self.modes
367                .iter()
368                .all(|mode| mode.is_partition_of(self.pixels.len())),
369            "every mode must stay a partition of the pixels"
370        );
371    }
372}
373
374#[cfg(test)]
375mod tests;