1mod color_order;
5mod edit;
6pub mod generators;
7mod model_presets;
8pub mod svg_geometry;
9
10pub use color_order::{ColorOrder, UnknownColorOrder};
11pub use edit::DefinitionError;
12
13use crate::asset::AssetTrait;
14use serde::{Deserialize, Serialize};
15use std::collections::{BTreeMap, BTreeSet, HashMap};
16use std::hash::Hash;
17use std::path::PathBuf;
18use strum::{Display, EnumIter, EnumMessage};
19
20pub const DEFAULT_PHOTO_OPACITY: f32 = 0.1;
22
23#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
26pub enum EmittedColor {
27 Red,
28 Green,
29 Blue,
30}
31
32#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
36pub struct Model {
37 pub name: String,
38 pub size_m: [f32; 2],
40 pub svg: String,
41}
42
43#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
44pub struct Pixel {
45 pub name: String,
46 pub model: usize,
48 pub position_m: [f32; 3],
50 pub rotation: f32,
52 pub channels: Vec<EmittedColor>,
53}
54
55impl Pixel {
56 pub fn new(name: impl Into<String>, position_m: [f32; 3], order: ColorOrder) -> Self {
58 Self {
59 name: name.into(),
60 model: 0,
61 position_m,
62 rotation: 0.0,
63 channels: order.channels().to_vec(),
64 }
65 }
66}
67
68pub fn group_ids<K: Eq + Hash>(keys: impl IntoIterator<Item = K>) -> Vec<usize> {
71 let mut ids = HashMap::new();
72 keys.into_iter()
73 .map(|key| {
74 let next = ids.len();
75 *ids.entry(key).or_insert(next)
76 })
77 .collect()
78}
79
80#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
87pub struct Mode {
88 pub name: String,
89 pub group_of: Vec<usize>,
90}
91
92impl Mode {
93 pub fn individual(pixel_count: usize) -> Self {
94 Self {
95 name: "individual".to_string(),
96 group_of: (0..pixel_count).collect(),
97 }
98 }
99
100 pub fn group_count(&self) -> usize {
102 self.group_of.iter().copied().collect::<BTreeSet<_>>().len()
103 }
104
105 pub fn groups(&self) -> BTreeMap<usize, Vec<usize>> {
107 let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
108 for (pixel, group) in self.group_of.iter().enumerate() {
109 groups.entry(*group).or_default().push(pixel);
110 }
111 groups
112 }
113}
114
115#[derive(
123 Serialize,
124 Deserialize,
125 Debug,
126 Clone,
127 Copy,
128 PartialEq,
129 Eq,
130 Default,
131 Display,
132 EnumIter,
133 EnumMessage,
134)]
135pub enum Scaling {
136 #[default]
139 #[strum(
140 serialize = "along its axis",
141 message = "Its size follows its own axis and its width keeps the proportions: a bar"
142 )]
143 AlongAxis,
144 #[strum(
147 serialize = "freely",
148 message = "Each axis scales on its own, even beyond what a room could show: a matrix"
149 )]
150 Free,
151}
152
153#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
156#[serde(default)]
157pub struct FixtureDefinition {
158 pub size_m: [f32; 3],
160 pub models: Vec<Model>,
161 pub outline: Option<usize>,
163 pub pixels: Vec<Pixel>,
164 pub modes: Vec<Mode>,
165 pub scaling: Scaling,
167 pub photo: Option<Photo>,
171}
172
173#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
177pub struct Photo {
178 pub file: PathBuf,
179 pub size_m: [f32; 2],
181 #[serde(default)]
183 pub offset_m: [f32; 2],
184 #[serde(default = "default_photo_opacity")]
186 pub opacity: f32,
187}
188
189fn default_photo_opacity() -> f32 {
190 DEFAULT_PHOTO_OPACITY
191}
192
193impl Photo {
194 pub fn reach_m(&self) -> [f32; 2] {
197 [0, 1].map(|axis| self.offset_m[axis].abs() + self.size_m[axis].abs() / 2.0)
198 }
199}
200
201impl Default for FixtureDefinition {
202 fn default() -> Self {
207 Self {
208 size_m: [0.0; 3],
209 models: vec![],
210 outline: None,
211 pixels: vec![],
212 modes: vec![Mode::individual(0)],
213 scaling: Scaling::default(),
214 photo: None,
215 }
216 }
217}
218
219impl FixtureDefinition {
220 pub fn channel_count(&self) -> u16 {
224 channel_sum(&self.pixels)
225 }
226
227 pub fn channel_offset(&self, pixel: usize) -> u16 {
230 channel_sum(&self.pixels[..pixel.min(self.pixels.len())])
231 }
232
233 pub fn default_mode(&self) -> usize {
235 0
236 }
237
238 pub fn mode(&self, index: usize) -> Option<&Mode> {
239 self.modes.get(index)
240 }
241
242 pub fn mode_names(&self) -> Vec<&str> {
243 self.modes.iter().map(|mode| mode.name.as_str()).collect()
244 }
245
246 pub fn model(&self, index: usize) -> Option<&Model> {
247 self.models.get(index)
248 }
249
250 pub fn non_rgb_pixel(&self) -> Option<&Pixel> {
253 self.pixels
254 .iter()
255 .find(|pixel| pixel.color_order().is_none())
256 }
257}
258
259impl AssetTrait for FixtureDefinition {
260 const DIR_NAME: &'static str = "fixture_definitions";
261 const NAME: &'static str = "Fixture Definition";
262 const SHOW_NAME_IF_SELECTED: bool = true;
263}
264
265fn channel_sum(pixels: &[Pixel]) -> u16 {
267 pixels.iter().fold(0u16, |sum, pixel| {
268 sum.saturating_add(u16::try_from(pixel.channels.len()).unwrap_or(u16::MAX))
269 })
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn a_photo_without_offset_or_opacity_sits_on_the_origin_and_shows_faintly() {
278 let photo: Photo =
279 serde_json::from_str(r#"{"file": "lamp.png", "size_m": [1.0, 0.5]}"#).expect("a photo");
280 assert_eq!(photo.offset_m, [0.0, 0.0]);
281 assert_eq!(photo.opacity, DEFAULT_PHOTO_OPACITY);
282 }
283
284 #[test]
285 fn a_photo_reaches_as_far_as_its_farthest_edge() {
286 let photo = Photo {
287 file: PathBuf::from("lamp.png"),
288 size_m: [1.0, 0.2],
289 offset_m: [0.25, -0.1],
290 opacity: 0.1,
291 };
292 let [x, y] = photo.reach_m();
293 assert!((x - 0.75).abs() < 1e-6 && (y - 0.2).abs() < 1e-6, "{x} {y}");
294 }
295
296 fn rgb_pixel(name: &str, x_m: f32) -> Pixel {
297 Pixel::new(name, [x_m, 0.0, 0.0], ColorOrder::Rgb)
298 }
299
300 #[test]
301 fn a_new_definition_is_empty_but_has_its_default_mode() {
302 let definition = FixtureDefinition::default();
303 assert!(definition.pixels.is_empty());
304 assert_eq!(definition.channel_count(), 0);
305 assert_eq!(definition.modes.len(), 1);
306 }
307
308 #[test]
309 fn channel_offsets_follow_the_pixel_order() {
310 let definition = FixtureDefinition {
311 pixels: vec![
312 rgb_pixel("a", 0.0),
313 rgb_pixel("b", 0.1),
314 rgb_pixel("c", 0.2),
315 ],
316 ..Default::default()
317 };
318 assert_eq!(definition.channel_count(), 9);
319 assert_eq!(definition.channel_offset(0), 0);
320 assert_eq!(definition.channel_offset(1), 3);
321 assert_eq!(definition.channel_offset(2), 6);
322 }
323
324 #[test]
326 fn a_definition_wider_than_u16_saturates_instead_of_overflowing() {
327 let definition = crate::test_support::rgb_bar(22_000, 1.0);
328 assert_eq!(definition.channel_count(), u16::MAX);
329 assert_eq!(definition.channel_offset(100), 300);
330 assert_eq!(definition.channel_offset(21_845), 65_535);
331 assert_eq!(definition.channel_offset(22_000), u16::MAX);
332
333 let mut wide = rgb_pixel("wide", 0.0);
334 wide.channels = vec![EmittedColor::Red; 70_000];
335 let definition = FixtureDefinition {
336 pixels: vec![rgb_pixel("a", 0.0), wide],
337 ..Default::default()
338 };
339 assert_eq!(definition.channel_count(), u16::MAX);
340 assert_eq!(definition.channel_offset(1), 3);
341 }
342
343 #[test]
344 fn group_ids_relabel_by_first_appearance() {
345 assert_eq!(group_ids([7, 7, 3, 7, 9, 3]), [0, 0, 1, 0, 2, 1]);
346 assert_eq!(group_ids(Vec::<usize>::new()), Vec::<usize>::new());
347 }
348
349 #[test]
350 fn individual_mode_is_the_identity() {
351 let mode = Mode::individual(4);
352 assert_eq!(mode.group_of, vec![0, 1, 2, 3]);
353 assert_eq!(mode.group_count(), 4);
354 }
355
356 #[test]
357 fn a_mode_groups_pixels() {
358 let mode = Mode {
360 name: "pairs".to_string(),
361 group_of: vec![0, 0, 1, 1, 2, 2],
362 };
363 assert_eq!(mode.group_count(), 3);
364 assert_eq!(mode.groups()[&0], vec![0, 1]);
365 assert_eq!(mode.groups()[&2], vec![4, 5]);
366 }
367
368 #[test]
369 fn a_pixel_that_is_no_rgb_permutation_is_detected() {
370 let mut pixel = rgb_pixel("w", 0.0);
371 pixel.channels.push(EmittedColor::Red);
372 let definition = FixtureDefinition {
373 pixels: vec![rgb_pixel("a", 0.0), pixel],
374 ..Default::default()
375 };
376 assert_eq!(
377 definition.non_rgb_pixel().map(|p| p.name.as_str()),
378 Some("w")
379 );
380 }
381
382 #[test]
384 fn a_permuted_rgb_pixel_is_drivable() {
385 let mut pixel = rgb_pixel("grb", 0.0);
386 pixel.set_color_order(ColorOrder::Grb);
387 let definition = FixtureDefinition {
388 pixels: vec![pixel],
389 ..Default::default()
390 };
391 assert!(definition.non_rgb_pixel().is_none());
392 }
393}