mod form;
use crate::fixture_definition_editor::edit::{DefinitionEdit, Edit};
use crate::fixture_definition_editor::state::EditorState;
use egui::{Id, Window};
use form::form;
use gled_model::patching::definition::generators::{
Corner, GridOrder, GridSpec, PathSpec, PixelTemplate, RingSpec, grid, path, ring, single,
};
use gled_model::patching::definition::{ColorOrder, Model, Pixel};
use strum::{Display, EnumIter};
const FIRST_LED_M: f32 = 0.01;
pub(crate) const MAX_LEDS: usize = 4096;
pub(crate) const DEFAULT_PIXEL_PREFIX: &str = "pixel-";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumIter)]
pub(crate) enum GeneratorKind {
Pixel,
#[strum(serialize = "Line…")]
Line,
#[strum(serialize = "Grid…")]
Grid,
#[strum(serialize = "Ring…")]
Ring,
}
#[derive(Debug, Clone)]
pub(crate) struct GeneratorParams {
prefix: String,
order: ColorOrder,
pixel_at_m: [f32; 2],
line_count: usize,
line_by_pitch: bool,
line_from_m: [f32; 2],
line_to_m: [f32; 2],
line_angle_deg: f32,
line_pitch_m: f32,
line_inset: bool,
grid: GridSpec,
ring: RingSpec,
}
impl Default for GeneratorParams {
fn default() -> Self {
Self {
prefix: DEFAULT_PIXEL_PREFIX.to_string(),
order: ColorOrder::Rgb,
pixel_at_m: [0.0, 0.0],
line_count: 10,
line_by_pitch: false,
line_from_m: [-0.5, 0.0],
line_to_m: [0.5, 0.0],
line_angle_deg: 0.0,
line_pitch_m: 0.1,
line_inset: true,
grid: GridSpec {
columns: 8,
rows: 8,
pitch_m: [0.03, 0.03],
centre_m: [0.0, 0.0],
order: GridOrder::SerpentineRows,
start: Corner::TopLeft,
},
ring: RingSpec {
centre_m: [0.0, 0.0],
radius_m: 0.2,
count: 12,
start_deg: 0.0,
clockwise: true,
along_ring: true,
},
}
}
}
impl GeneratorParams {
fn capped_grid(&self) -> GridSpec {
GridSpec {
rows: self.grid.rows.min(max_rows(self.grid.columns)),
..self.grid
}
}
fn pixels(&self, kind: GeneratorKind, template: &PixelTemplate) -> Vec<Pixel> {
match kind {
GeneratorKind::Pixel => single(template, self.pixel_at_m),
GeneratorKind::Line => {
let (to_m, inset_ends) = if self.line_by_pitch {
let (sin, cos) = self.line_angle_deg.to_radians().sin_cos();
let span = self.line_pitch_m * self.line_count.saturating_sub(1) as f32;
let from = self.line_from_m;
([from[0] + cos * span, from[1] + sin * span], false)
} else {
(self.line_to_m, self.line_inset)
};
let spec = PathSpec {
points_m: vec![self.line_from_m, to_m],
closed: false,
count: self.line_count,
inset_ends,
};
path(template, &spec)
}
GeneratorKind::Grid => grid(template, &self.capped_grid()),
GeneratorKind::Ring => ring(template, &self.ring),
}
}
}
pub(crate) fn template(state: &EditorState, next_number: usize) -> PixelTemplate {
PixelTemplate {
prefix: state.params.prefix.clone(),
first_number: next_number,
model: state.current_model,
order: state.params.order,
}
}
pub(crate) fn preview(state: &EditorState, next_number: usize) -> Vec<Pixel> {
state
.generator
.map(|kind| state.params.pixels(kind, &template(state, next_number)))
.unwrap_or_default()
}
pub(crate) fn start(
kind: GeneratorKind,
edit: &mut Edit<'_>,
state: &mut EditorState,
at_m: [f32; 2],
) {
if kind == GeneratorKind::Pixel {
state.params.pixel_at_m = at_m;
add(GeneratorKind::Pixel, edit, state);
} else {
state.generator = Some(kind);
}
}
fn add(kind: GeneratorKind, edit: &mut Edit<'_>, state: &mut EditorState) {
let params = state.params.clone();
add_pixels(edit, state, |template| params.pixels(kind, template));
}
pub(crate) fn add_pixels(
edit: &mut Edit<'_>,
state: &mut EditorState,
make: impl FnOnce(&PixelTemplate) -> Vec<Pixel>,
) {
let mut template = template(state, edit.value.next_pixel_number());
let added = edit.change(state, |definition| {
if definition.models.is_empty() {
definition.models.push(Model::circle("led", FIRST_LED_M));
template.model = 0;
}
definition.insert_pixels(make(&template))
});
state.selection = added.collect();
}
fn max_rows(columns: usize) -> usize {
(MAX_LEDS / columns.max(1)).max(1)
}
pub(crate) fn show(ctx: &egui::Context, edit: &mut Edit<'_>, state: &mut EditorState) {
let Some(kind) = state.generator else {
return;
};
let mut chosen = None;
let title = format!(
"Add {}",
kind.to_string().trim_end_matches('…').to_lowercase()
);
let corner = state.canvas_rect.left_top() + egui::vec2(8.0, 8.0);
Window::new(title)
.id(Id::new("fixture definition generator"))
.fixed_pos(corner)
.collapsible(false)
.resizable(false)
.show(ctx, |ui| {
ui.set_width(340.0);
form(ui, kind, &mut state.params);
let pixels = state.params.pixels(kind, &template(state, 0)).len();
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui.button(format!("Add {pixels} pixels")).clicked() {
chosen = Some(true);
}
if ui.button("Cancel").clicked() {
chosen = Some(false);
}
});
});
if chosen.is_none() && !state.typing && !ctx.text_edit_focused() {
ctx.input_mut(|input| {
if input.consume_key(egui::Modifiers::NONE, egui::Key::Enter) {
chosen = Some(true);
} else if input.consume_key(egui::Modifiers::NONE, egui::Key::Escape) {
chosen = Some(false);
}
});
}
match chosen {
Some(true) => {
add(kind, edit, state);
state.generator = None;
}
Some(false) => state.generator = None,
None => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_grid_stays_within_the_led_limit() {
let mut params = GeneratorParams::default();
params.grid.columns = MAX_LEDS;
params.grid.rows = MAX_LEDS;
let template = PixelTemplate {
prefix: DEFAULT_PIXEL_PREFIX.to_string(),
first_number: 0,
model: 0,
order: ColorOrder::Rgb,
};
assert_eq!(
params.pixels(GeneratorKind::Grid, &template).len(),
MAX_LEDS
);
}
}