use crate::canvas::caches::Caches;
use crate::canvas::model_textures::{
ModelBatch, ModelKey, aspect_bucket, draw_placeholder, svg_hash,
};
use crate::canvas::rotated_rect::RotatedRect;
use crate::canvas::view::fit_into;
use egui::emath::Vec2;
use egui::emath::{Rot2, TSTransform};
use egui::epaint::Color32;
use egui::{Painter, Pos2, Rect, vec2};
use gled_model::patching::definition::{FixtureDefinition, Model};
use gled_model::patching::fixture::{LayoutPosition, UNITS_PER_METRE};
pub(crate) const MIN_FIXTURE_SCREEN_PX: f32 = 11.0;
pub(crate) fn mode_color(mode: Option<usize>) -> Color32 {
let Some(mode) = mode else {
return Color32::from_rgb(120, 60, 60);
};
static COLORS: &[Color32] = &[
Color32::from_rgb(175, 213, 129),
Color32::from_rgb(177, 152, 221),
Color32::from_rgb(140, 181, 255),
Color32::from_rgb(217, 176, 140),
Color32::from_rgb(217, 148, 140),
Color32::from_rgb(113, 208, 132),
Color32::from_rgb(213, 129, 192),
Color32::from_rgb(163, 218, 224),
Color32::from_rgb(222, 233, 190),
Color32::from_rgb(255, 255, 255),
];
COLORS[mode % COLORS.len()]
}
pub(crate) fn legibility_scale(
definition: &FixtureDefinition,
position: &LayoutPosition,
zoom: f32,
min_screen_px: f32,
) -> Vec2 {
let size = position.scaled([definition.size_m[0], definition.size_m[1]]) * zoom;
let floor = |extent: f32| (min_screen_px / extent.abs().max(f32::EPSILON)).max(1.0);
vec2(floor(size.x), floor(size.y))
}
pub(crate) fn fixture_footprint(
definition: &FixtureDefinition,
position: &LayoutPosition,
zoom: f32,
min_screen_px: f32,
) -> RotatedRect {
let size = position.scaled([definition.size_m[0], definition.size_m[1]]);
let legible = legibility_scale(definition, position, zoom, min_screen_px);
RotatedRect {
centre: position.pos,
half: vec2(size.x.abs() * legible.x, size.y.abs() * legible.y) * 0.5,
angle: position.angle,
}
}
fn aspect_of(model: &Model, position: &LayoutPosition) -> i32 {
let size = position.scaled(model.size_m);
aspect_bucket(size.x.abs() / size.y.abs().max(f32::EPSILON))
}
fn model_key(
definition_id: Option<uuid::Uuid>,
index: usize,
model: &Model,
position: &LayoutPosition,
) -> ModelKey {
(
definition_id,
index,
aspect_of(model, position),
svg_hash(model),
)
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum PixelTint<'a> {
Uniform(Color32),
PerPixel(&'a [Color32]),
}
impl PixelTint<'_> {
fn of(&self, pixel: usize) -> Color32 {
match self {
Self::Uniform(color) => *color,
Self::PerPixel(colors) => colors
.get(pixel)
.or(colors.last())
.copied()
.unwrap_or(EDITOR_PIXEL),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Placement<'a> {
pub(crate) position: &'a LayoutPosition,
pub(crate) to_global: TSTransform,
pub(crate) min_screen_px: f32,
}
impl Placement<'_> {
fn legible(&self, definition: &FixtureDefinition) -> Vec2 {
legibility_scale(
definition,
self.position,
self.to_global.scaling,
self.min_screen_px,
)
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Style<'a> {
pub(crate) outline: Color32,
pub(crate) pixels: PixelTint<'a>,
}
pub(crate) fn draw_photo(
painter: &Painter,
definition: &FixtureDefinition,
placement: &Placement<'_>,
caches: &mut Caches,
) {
let Some(photo) = &definition.photo else {
return;
};
let Some(texture) = caches.images.texture(painter.ctx(), &photo.file) else {
return;
};
let Placement {
position,
to_global,
..
} = *placement;
let legible = placement.legible(definition);
let offset = position.scaled(photo.offset_m) * legible;
let centre = position.pos + Rot2::from_angle(position.angle) * offset;
let size = position.scaled(photo.size_m) * to_global.scaling * legible;
let mut batch = ModelBatch::new(&texture);
batch.push(
to_global * centre,
size,
position.angle,
Color32::WHITE.gamma_multiply(photo.opacity.clamp(0.0, 1.0)),
);
batch.paint(painter);
}
pub(crate) fn draw_outline(
painter: &Painter,
definition: &FixtureDefinition,
definition_id: Option<uuid::Uuid>,
placement: &Placement<'_>,
color: Color32,
caches: &mut Caches,
) {
let Placement {
position,
to_global,
min_screen_px,
} = *placement;
let legible = placement.legible(definition);
let outline = definition
.outline
.and_then(|index| Some((index, definition.model(index)?)));
match outline.and_then(|(index, model)| {
let key = model_key(definition_id, index, model, position);
Some((caches.models.texture(painter.ctx(), key, model)?, model))
}) {
Some((texture, model)) => {
let size = position.scaled(model.size_m) * to_global.scaling * legible;
let mut batch = ModelBatch::new(&texture);
batch.push(to_global * position.pos, size, position.angle, color);
batch.paint(painter);
}
None => draw_placeholder(
painter,
fixture_footprint(definition, position, to_global.scaling, min_screen_px)
.transformed(to_global)
.corners()
.to_vec(),
color,
),
}
}
pub(crate) fn draw_definition(
painter: &Painter,
definition: &FixtureDefinition,
definition_id: Option<uuid::Uuid>,
placement: &Placement<'_>,
style: Style<'_>,
caches: &mut Caches,
) {
draw_outline(
painter,
definition,
definition_id,
placement,
style.outline,
caches,
);
let Placement {
position,
to_global,
..
} = *placement;
let legible = placement.legible(definition);
let rotation = Rot2::from_angle(position.angle);
let spread = |centre: &Pos2| {
let offset = rotation.inverse() * (*centre - position.pos);
position.pos + rotation * vec2(offset.x * legible.x, offset.y * legible.y)
};
let positions = definition.pixel_scene_positions(position);
let mut batches: Vec<(usize, ModelBatch)> = vec![];
for (index, pixel) in definition.pixels.iter().enumerate() {
let (Some(model), Some(centre)) = (definition.model(pixel.model), positions.get(index))
else {
continue;
};
if !batches
.iter()
.any(|(model_index, _)| *model_index == pixel.model)
{
let key = model_key(definition_id, pixel.model, model, position);
let Some(texture) = caches.models.texture(painter.ctx(), key, model) else {
continue;
};
batches.push((pixel.model, ModelBatch::new(&texture)));
}
let Some((_, batch)) = batches
.iter_mut()
.find(|(model_index, _)| *model_index == pixel.model)
else {
continue;
};
let size = position.scaled(model.size_m) * to_global.scaling * legible;
batch.push(
to_global * spread(centre),
size,
position.angle + pixel.rotation,
style.pixels.of(index),
);
}
for (_, batch) in batches {
batch.paint(painter);
}
}
pub(crate) fn draw_definition_fitted(
painter: &Painter,
rect: Rect,
definition: &FixtureDefinition,
definition_id: Option<uuid::Uuid>,
caches: &mut Caches,
) {
let scene = vec2(
(definition.size_m[0] * UNITS_PER_METRE).abs().max(1.0),
(definition.size_m[1] * UNITS_PER_METRE).abs().max(1.0),
);
let scale = fit_into(scene, rect).width() / scene.x;
let placement = Placement {
position: &LayoutPosition::at(Pos2::ZERO),
to_global: TSTransform::new(rect.center().to_vec2(), scale),
min_screen_px: 1.0,
};
let style = Style {
outline: Color32::from_gray(210),
pixels: PixelTint::Uniform(EDITOR_PIXEL),
};
draw_definition(
painter,
definition,
definition_id,
&placement,
style,
caches,
);
}
pub(crate) const EDITOR_PIXEL: Color32 = Color32::from_rgb(18, 18, 20);
pub(crate) fn reach_m(definition: &FixtureDefinition) -> f32 {
let pixels = definition.pixel_bounds_m().map_or(0.0, |(min, max)| {
[min[0], min[1], max[0], max[1]]
.into_iter()
.fold(0.0f32, |reach, value| reach.max(value.abs()))
});
pixels
.max(definition.size_m[0] / 2.0)
.max(definition.size_m[1] / 2.0)
}
pub(crate) fn reach_with_photo_m(definition: &FixtureDefinition) -> f32 {
let photo = definition.photo.as_ref().map_or(0.0, |photo| {
let [x, y] = photo.reach_m();
x.max(y)
});
reach_m(definition).max(photo)
}
#[cfg(test)]
mod tests {
use super::*;
use egui::pos2;
fn bar() -> FixtureDefinition {
FixtureDefinition {
size_m: [1.0, 0.05, 0.05],
..Default::default()
}
}
#[test]
fn a_tiny_fixture_is_blown_up_to_stay_legible() {
let definition = bar();
let position = LayoutPosition::at(pos2(0.0, 0.0));
let zoomed_out = legibility_scale(&definition, &position, 0.45, MIN_FIXTURE_SCREEN_PX);
assert_eq!(zoomed_out.x, 1.0, "the long axis is already legible");
assert!(zoomed_out.y > 4.0, "the short axis is not: {zoomed_out:?}");
let zoomed_in = legibility_scale(&definition, &position, 8.0, MIN_FIXTURE_SCREEN_PX);
assert_eq!(zoomed_in, egui::vec2(1.0, 1.0));
}
}