use crate::canvas::definition_drawing::reach_m;
use crate::canvas::rotated_rect::RotatedRect;
use crate::canvas::view::fit_scene_square;
use egui::emath::TSTransform;
use egui::{Color32, Painter, Pos2, Rect, Shape, Stroke, StrokeKind, Ui, vec2};
use gled_gpu::group_samples::GroupSamples;
use gled_gpu::patch_positions::PatchPositions;
use gled_gpu::render_to_output::positions::Lamp;
use gled_model::collection::Collections;
use gled_model::patching::Patch;
use gled_model::patching::definition::FixtureDefinition;
use gled_model::patching::fixture::{FixtureId, LayoutPosition, UNITS_PER_METRE};
use gled_model::patching::layout::{LayoutSelection, scene_bounds};
use std::collections::BTreeMap;
const BACKDROP_CELLS: usize = 40;
const UNLIT: Color32 = Color32::from_gray(28);
const LINE_WIDTH: f32 = 0.25;
const SWEEP_ACROSS: f64 = 5.0;
const SWEEP_DOWN: f64 = 7.0;
pub(crate) fn test_pattern(uv: egui::Vec2, time: f64) -> Color32 {
let across = sweep(uv.x, time / SWEEP_ACROSS);
let down = sweep(uv.y, time / SWEEP_DOWN);
let channel = |value: f32| (255.0 * value.clamp(0.0, 1.0)) as u8;
Color32::from_rgb(channel(across), channel(down), channel(across * down))
}
fn sweep(at: f32, phase: f64) -> f32 {
let distance = (at - phase.rem_euclid(1.0) as f32).abs();
let distance = distance.min(1.0 - distance);
(1.0 - distance / (LINE_WIDTH / 2.0)).max(0.0)
}
struct Screen {
painter: Painter,
time: f64,
to_screen: TSTransform,
scale: f32,
}
impl Screen {
fn new(ui: &mut Ui, rect: Rect) -> Self {
let painter = ui.painter_at(rect);
let time = ui.input(|i| i.time);
ui.ctx().request_repaint();
let to_screen = fit_scene_square(rect);
let screen = to_screen * scene_bounds();
let scale = to_screen.scaling;
draw_backdrop(&painter, screen, time);
painter.rect_stroke(
screen,
0.0,
Stroke::new(1.0, Color32::from_gray(70)),
StrokeKind::Inside,
);
Self {
painter,
time,
to_screen,
scale,
}
}
fn draw_pixel(
&self,
definition: &FixtureDefinition,
position: &LayoutPosition,
pixel: usize,
centre: Pos2,
color: Color32,
) {
let Some(found) = definition.pixels.get(pixel) else {
return;
};
let Some(model) = definition.model(found.model) else {
return;
};
let size = position.scaled(model.size_m) * self.scale;
let half = vec2(size.x.abs().max(2.0), size.y.abs().max(2.0)) * 0.5;
let shape = RotatedRect {
centre: self.to_screen * centre,
half,
angle: position.angle + found.rotation,
};
self.painter.add(Shape::convex_polygon(
shape.corners().to_vec(),
color,
Stroke::NONE,
));
}
}
fn pixel_colors(
patch: &Patch,
selection: LayoutSelection,
collections: &Collections,
time: f64,
) -> BTreeMap<FixtureId, Vec<Color32>> {
let positions = patch.positions(selection, collections).unwrap_or_default();
let addressing = patch.addressing(collections);
patch
.topology
.default_layout
.members
.keys()
.filter_map(|id| {
let definition = patch.definition(*id, collections)?;
let definition = &definition.data;
let slot = addressing.slot(*id);
let colors = (0..definition.pixels.len())
.map(|pixel| {
let lamp = slot.and_then(|(slot, address)| {
let index = address.lamp_index(definition.channel_offset(pixel));
positions.lamp(slot, index)
});
match lamp {
Some(Lamp::Position { x, y }) => test_pattern(vec2(x, 1.0 - y), time),
_ => UNLIT,
}
})
.collect();
Some((*id, colors))
})
.collect()
}
pub(crate) fn show(
ui: &mut Ui,
rect: Rect,
patch: &Patch,
selection: LayoutSelection,
collections: &Collections,
) {
let screen = Screen::new(ui, rect);
let colors = pixel_colors(patch, selection, collections, screen.time);
for (id, position) in &patch.topology.default_layout.members {
let (Some(definition), Some(colors)) = (patch.definition(*id, collections), colors.get(id))
else {
continue;
};
let definition = &definition.data;
let centres = definition.pixel_scene_positions(position);
for (pixel, (centre, color)) in centres.iter().zip(colors).enumerate() {
screen.draw_pixel(definition, position, pixel, *centre, *color);
}
}
}
const DEFINITION_FILL: f32 = 0.8;
pub(crate) fn show_definition(
ui: &mut Ui,
rect: Rect,
definition: &FixtureDefinition,
mode: usize,
) {
let screen = Screen::new(ui, rect);
let position = definition_position(definition);
let samples = GroupSamples::new(definition, &position, mode);
for (pixel, color) in definition_pixel_colors(&samples, screen.time) {
if let Some(centre) = samples.pixel_positions.get(pixel) {
screen.draw_pixel(definition, &position, pixel, *centre, color);
}
}
}
fn definition_position(definition: &FixtureDefinition) -> LayoutPosition {
let bounds = scene_bounds();
let longest = (2.0 * reach_m(definition)).max(0.01);
let scale = DEFINITION_FILL * bounds.width() / (longest * UNITS_PER_METRE);
LayoutPosition::new(bounds.center(), 0.0, [scale, scale])
}
fn definition_pixel_colors(samples: &GroupSamples, time: f64) -> Vec<(usize, Color32)> {
let bounds = scene_bounds();
samples
.groups
.iter()
.flat_map(|group| {
let uv = vec2(
group.centre.x / bounds.width(),
group.centre.y / bounds.height(),
);
let color = test_pattern(uv, time);
group.pixels.iter().map(move |pixel| (*pixel, color))
})
.collect()
}
fn draw_backdrop(painter: &Painter, screen: Rect, time: f64) {
let cell = screen.size() / BACKDROP_CELLS as f32;
let mut mesh = egui::epaint::Mesh::default();
for y in 0..BACKDROP_CELLS {
for x in 0..BACKDROP_CELLS {
let uv = vec2(
(x as f32 + 0.5) / BACKDROP_CELLS as f32,
(y as f32 + 0.5) / BACKDROP_CELLS as f32,
);
let color = test_pattern(uv, time).gamma_multiply(0.22);
let rect = Rect::from_min_size(
screen.min + vec2(x as f32 * cell.x, y as f32 * cell.y),
cell,
);
mesh.add_colored_rect(rect, color);
}
}
painter.add(Shape::mesh(mesh));
}
#[cfg(test)]
mod tests {
use super::*;
use gled_model::patching::layout::LayoutRef;
#[test]
fn the_two_lines_cross_where_they_meet() {
assert_eq!(test_pattern(vec2(0.0, 0.0), 0.0), Color32::WHITE);
let across = test_pattern(vec2(0.0, 0.5), 0.0);
assert!(across.r() > 200 && across.g() == 0, "{across:?}");
let down = test_pattern(vec2(0.5, 0.0), 0.0);
assert!(down.g() > 200 && down.r() == 0, "{down:?}");
assert_eq!(
test_pattern(vec2(0.5, 0.5), 0.0),
Color32::BLACK,
"between the lines the square is dark"
);
}
#[test]
fn the_lines_sweep_with_time() {
let quarter = SWEEP_ACROSS * 0.25;
let lit = test_pattern(vec2(0.25, 0.5), quarter);
assert!(lit.r() > 200, "a quarter of the way across: {lit:?}");
assert_eq!(
test_pattern(vec2(0.0, 0.5), quarter).r(),
0,
"and gone from where it started"
);
}
#[test]
fn a_line_covers_a_quarter_of_the_square() {
let lit = |x: f32| test_pattern(vec2(x, 0.5), 0.0).r() > 0;
assert!(lit(LINE_WIDTH / 2.0 - 0.01), "just inside its edge");
assert!(!lit(LINE_WIDTH / 2.0 + 0.01), "just outside it");
}
fn half_bar() -> FixtureDefinition {
let mut bar = gled_model::test_support::rgb_bar(4, 1.0);
bar.modes.push(gled_model::patching::definition::Mode {
name: "half".to_string(),
group_of: vec![0, 0, 1, 1],
});
bar
}
fn rig() -> (Patch, Collections, FixtureId, FixtureId) {
use gled_model::patching::fixture::Fixture;
use gled_model::test_support::collections_with;
let (collections, ids) = collections_with(vec![half_bar()]);
let mut patch = Patch::default();
let mut add = |name| {
let mut fixture = Fixture::new(name);
fixture.definition = Some(ids[0]);
patch.add_fixture(fixture, LayoutPosition::at(scene_bounds().center()))
};
let (first, second) = (add("first"), add("second"));
patch.topology.make_successor(first, second);
(patch, collections, first, second)
}
#[test]
fn a_fixture_the_layout_leaves_out_stays_dark() {
let (mut patch, collections, first, second) = rig();
let layout = LayoutRef::Layout(patch.add_layout());
patch.add_member(layout, first);
let selection = LayoutSelection::new(layout, 0);
let colors = pixel_colors(&patch, selection, &collections, 0.0);
assert!(colors[&first].iter().all(|color| *color != UNLIT));
assert!(colors[&second].iter().all(|color| *color == UNLIT));
}
#[test]
fn a_group_is_lit_in_one_colour() {
let (mut patch, collections, first, _) = rig();
patch.set_fixture_mode(LayoutRef::Topology, 0, first, 1);
let selection = LayoutSelection::new(LayoutRef::Topology, 0);
let colors = pixel_colors(&patch, selection, &collections, 0.3);
let first = &colors[&first];
assert_eq!(first[0], first[1]);
assert_eq!(first[2], first[3]);
assert!(first.iter().all(|color| *color != UNLIT));
}
#[test]
fn a_definition_group_is_lit_in_one_colour() {
let bar = half_bar();
let samples = GroupSamples::new(&bar, &definition_position(&bar), 1);
let colors: BTreeMap<usize, Color32> =
definition_pixel_colors(&samples, 0.3).into_iter().collect();
assert_eq!(colors.len(), 4, "every pixel is drawn: {colors:?}");
assert_eq!(colors[&0], colors[&1]);
assert_eq!(colors[&2], colors[&3]);
}
#[test]
fn a_line_wraps_at_the_edge() {
let almost_round = SWEEP_ACROSS * 0.99;
let lit = test_pattern(vec2(0.0, 0.5), almost_round);
assert!(lit.r() > 0, "{lit:?}");
}
}