gled-ui 2.31.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Painting the definition canvas: the grid, the photo, the definition with
//! its DMX numbers, the selection, and what a tool would add.

use super::Frame;
use crate::canvas::PREVIEW_COLOR;
use crate::canvas::box_select::draw_band;
use crate::canvas::definition_drawing::{
    PixelTint, Placement, Style, draw_definition, draw_photo, mode_color,
};
use crate::canvas::rotated_rect::RotatedRect;
use crate::canvas::shapes::{Shapes, draw_adaptive_grid};
use crate::fixture_definition_editor::canvas_geometry::{
    label_font_px, pixel_shape, pixel_shape_of, pixel_under, to_metres,
};
use crate::fixture_definition_editor::draw_tools;
use crate::fixture_definition_editor::generators;
use crate::fixture_definition_editor::state::Drag;
use egui::{
    Align2, Color32, FontId, Painter, Pos2, Rect, Response, Shape, Stroke, Vec2, pos2, vec2,
};
use gled_model::patching::fixture::{LayoutPosition, UNITS_PER_METRE};

/// Pixels in "individual", where every pixel is its own group.
const INDIVIDUAL_PIXEL: Color32 = Color32::from_gray(170);
const OUTLINE: Color32 = Color32::from_gray(110);
const SELECTED: Color32 = Color32::WHITE;
const HOVERED: Color32 = Color32::from_gray(140);

impl Frame<'_, '_> {
    pub(super) fn paint(&mut self, painter: &Painter, asset_id: uuid::Uuid, response: &Response) {
        let origin = self.state.origin();
        let definition = &*self.edit.value;
        let to_screen = self.scene_transform;
        let min_half = self.min_half();

        painter.add(self.grid(origin));
        let placement = Placement {
            position: &LayoutPosition::at(origin),
            to_global: to_screen,
            min_screen_px: 0.0,
        };
        draw_photo(painter, definition, &placement, &mut self.state.caches);
        if definition.pixels.is_empty() && self.state.draw.shape.is_none() {
            painter.text(
                painter.clip_rect().center() - vec2(0.0, 40.0),
                Align2::CENTER_CENTER,
                "Draw LEDs: L line · C circle · P path\nor double-click to place one",
                FontId::proportional(16.0),
                Color32::from_gray(150),
            );
        }

        let mode = definition
            .mode(self.state.mode)
            .filter(|_| self.state.mode > 0);
        let colors: Vec<Color32> = mode
            .map(|mode| {
                mode.group_of
                    .iter()
                    .map(|group| mode_color(Some(*group)))
                    .collect()
            })
            .unwrap_or_default();
        let style = Style {
            outline: OUTLINE,
            pixels: if mode.is_some() {
                PixelTint::PerPixel(&colors)
            } else {
                PixelTint::Uniform(INDIVIDUAL_PIXEL)
            },
        };
        draw_definition(
            painter,
            definition,
            Some(asset_id),
            &placement,
            style,
            &mut self.state.caches,
        );

        // DMX numbers, where they fit
        for pixel in 0..definition.pixels.len() {
            let Some(shape) = pixel_shape(definition, pixel, origin, 0.0) else {
                continue;
            };
            let size_px = 2.0 * shape.half.min_elem() * to_screen.scaling;
            if let Some(font) = label_font_px(size_px) {
                painter.text(
                    to_screen * shape.centre,
                    Align2::CENTER_CENTER,
                    (pixel + 1).to_string(),
                    FontId::proportional(font),
                    Color32::BLACK,
                );
            }
        }

        let outline = |shape: RotatedRect, stroke: Stroke| {
            Shape::closed_line(
                shape.corners().map(|corner| to_screen * corner).to_vec(),
                stroke,
            )
        };
        let hovered = response
            .hover_pos()
            .filter(|_| self.state.drag.is_none())
            .and_then(|pointer| pixel_under(definition, origin, pointer, min_half));
        if let Some(shape) =
            hovered.and_then(|pixel| pixel_shape(definition, pixel, origin, min_half))
        {
            painter.add(outline(shape, Stroke::new(1.5, HOVERED)));
        }
        for pixel in &self.state.selection {
            if let Some(shape) = pixel_shape(definition, *pixel, origin, min_half) {
                painter.add(outline(shape, Stroke::new(2.0, SELECTED)));
            }
        }

        // what an open generator form or the shape being drawn would add
        let model_size = |model: usize| {
            definition
                .model(model)
                .map(|model| model.size_m)
                .or(Some([0.01, 0.01]))
        };
        let pointer_m = response
            .hover_pos()
            .map(|pointer| to_metres(origin, pointer));
        draw_tools::paint(painter, to_screen, origin, self.state, pointer_m);
        let previews = generators::preview(self.state, definition.next_pixel_number())
            .into_iter()
            .chain(draw_tools::preview(self.edit, self.state));
        for pixel in previews {
            let shape = pixel_shape_of(&pixel, model_size(pixel.model), origin, min_half);
            painter.add(outline(shape, Stroke::new(1.0, PREVIEW_COLOR)));
        }

        if let Some(Drag::Band(band)) = &self.state.drag {
            draw_band(&mut self.overlay, &to_screen, band.rect());
        }
        painter.add(std::mem::take(&mut self.overlay).into_shape());

        if let (Some(pixel), Some(pointer)) = (hovered, response.hover_pos()) {
            self.tooltip(painter, pixel, to_screen * pointer);
        }
    }

    /// Centimetre, decimetre and metre lines, the axes through the fixture's
    /// origin and its size as a dashed box.
    fn grid(&self, origin: Pos2) -> Shape {
        let to_screen = self.scene_transform;
        let side = self.state.scene_side;
        let scene = Rect::from_min_size(Pos2::ZERO, Vec2::splat(side));
        let mut under = Shapes::default();
        let line = |alpha| Stroke::new(0.5, Color32::from_white_alpha(alpha));
        draw_adaptive_grid(
            &mut under,
            scene,
            &to_screen,
            &[(1.0, line(16)), (10.0, line(32)), (100.0, line(64))],
        );
        let axis = Stroke::new(1.0, Color32::from_white_alpha(90));
        under.line_segment(
            [
                to_screen * pos2(0.0, origin.y),
                to_screen * pos2(side, origin.y),
            ],
            axis,
        );
        under.line_segment(
            [
                to_screen * pos2(origin.x, 0.0),
                to_screen * pos2(origin.x, side),
            ],
            axis,
        );

        let size = &self.edit.value.size_m;
        let footprint = Rect::from_center_size(origin, vec2(size[0], size[1]) * UNITS_PER_METRE);
        if footprint.area() > 0.0 {
            let corners = [
                footprint.left_top(),
                footprint.right_top(),
                footprint.right_bottom(),
                footprint.left_bottom(),
                footprint.left_top(),
            ]
            .map(|corner| to_screen * corner);
            under.add(Shape::dashed_line(
                &corners,
                Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 210, 90, 120)),
                6.0,
                4.0,
            ));
        }
        under.into_shape()
    }

    /// Name, channels and group of the pixel under the pointer.
    fn tooltip(&self, painter: &Painter, pixel: usize, pointer: Pos2) {
        let definition = &*self.edit.value;
        let Some(found) = definition.pixels.get(pixel) else {
            return;
        };
        // saturating: a malformed definition can hold more channels than u16
        let first = definition.channel_offset(pixel).saturating_add(1);
        let width = u16::try_from(found.channels.len()).unwrap_or(u16::MAX);
        let last = first.saturating_add(width).saturating_sub(1);
        let order = found
            .color_order()
            .map_or("custom".to_string(), |order| order.to_string());
        let mut text = format!("{} · ch {first}–{last} · {order}", found.name);
        if let Some(mode) = definition
            .mode(self.state.mode)
            .filter(|_| self.state.mode > 0)
            && let Some(group) = mode.group_of.get(pixel)
        {
            text.push_str(&format!(" · group {}", group + 1));
        }
        let at = pointer + vec2(14.0, 14.0);
        let galley = painter.layout_no_wrap(text, FontId::proportional(12.0), Color32::WHITE);
        let background = Rect::from_min_size(at, galley.size()).expand(4.0);
        painter.rect_filled(background, 3.0, Color32::from_black_alpha(200));
        painter.galley(at, galley, Color32::WHITE);
    }
}