use crate::canvas::images::sane_aspect;
use crate::canvas::rotated_rect::RotatedRect;
use egui::epaint::{Mesh, Vertex};
use egui::{
Color32, ColorImage, Context, Painter, Pos2, Shape, TextureHandle, TextureOptions, Vec2, pos2,
};
use gled_model::patching::definition::Model;
use gled_svg::{ParsedSvg, RenderedSvg};
use std::collections::HashMap;
pub(crate) fn color_image(svg: &RenderedSvg) -> ColorImage {
ColorImage::from_rgba_premultiplied([svg.width, svg.height], &svg.rgba)
}
const RASTER_SIZE: f32 = 96.0;
const MIN_RASTER_EDGE: f32 = 12.0;
const MAX_RASTER_SIZE: f32 = 2048.0;
const KEPT_MODELS: usize = 64;
const IDLE_FRAMES: u64 = 300;
fn raster_size(aspect: f32) -> (u32, u32) {
let aspect = sane_aspect(aspect);
let elongation = aspect.max(1.0 / aspect);
let long = (MIN_RASTER_EDGE * elongation).clamp(RASTER_SIZE, MAX_RASTER_SIZE);
let short = (long / elongation).max(1.0);
let (width, height) = if aspect >= 1.0 {
(long, short)
} else {
(short, long)
};
(width.round() as u32, height.round() as u32)
}
pub(crate) type ModelKey = (Option<uuid::Uuid>, usize, i32, u64);
pub(crate) fn svg_hash(model: &Model) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
model.svg.hash(&mut hasher);
hasher.finish()
}
pub(crate) fn aspect_bucket(aspect: f32) -> i32 {
if !aspect.is_finite() || aspect <= 0.0 {
return 0;
}
(aspect.log2() * 4.0).round() as i32
}
fn bucket_aspect(bucket: i32) -> f32 {
2f32.powf(bucket as f32 / 4.0)
}
#[derive(Default)]
pub(crate) struct ModelTextures {
textures: HashMap<ModelKey, (Option<TextureHandle>, u64)>,
frame: u64,
}
impl std::fmt::Debug for ModelTextures {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModelTextures")
.field("textures", &self.textures.len())
.finish_non_exhaustive()
}
}
impl ModelTextures {
pub(crate) fn begin_frame(&mut self) {
self.frame += 1;
if self.textures.len() > KEPT_MODELS {
let frame = self.frame;
self.textures
.retain(|_, (_, last_used)| frame - *last_used <= IDLE_FRAMES);
}
}
pub(crate) fn texture(
&mut self,
ctx: &Context,
key: ModelKey,
model: &Model,
) -> Option<TextureHandle> {
let frame = self.frame;
let (texture, last_used) = self
.textures
.entry(key)
.or_insert_with(|| (rasterize(ctx, key, model), frame));
*last_used = self.frame;
texture.clone()
}
}
fn rasterize(ctx: &Context, key: ModelKey, model: &Model) -> Option<TextureHandle> {
let (width, height) = raster_size(bucket_aspect(key.2));
let image = ParsedSvg::parse(&model.svg)
.and_then(|svg| svg.render(width, height))
.map_err(|error| tracing::warn!("Could not rasterize model \"{}\": {error}", model.name))
.ok()?;
Some(ctx.load_texture(
format!("fixture model {}", model.name),
color_image(&image),
TextureOptions::LINEAR,
))
}
pub(crate) struct ModelBatch {
mesh: Mesh,
}
impl ModelBatch {
pub(crate) fn new(texture: &TextureHandle) -> Self {
Self {
mesh: Mesh::with_texture(texture.id()),
}
}
pub(crate) fn push(&mut self, centre: Pos2, size: Vec2, angle: f32, tint: Color32) {
let corners = RotatedRect {
centre,
half: size * 0.5,
angle,
}
.corners();
let uv = [
pos2(0.0, 0.0),
pos2(1.0, 0.0),
pos2(1.0, 1.0),
pos2(0.0, 1.0),
];
let base = self.mesh.vertices.len() as u32;
for (pos, uv) in corners.into_iter().zip(uv) {
self.mesh.vertices.push(Vertex {
pos,
uv,
color: tint,
});
}
self.mesh
.indices
.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
}
pub(crate) fn paint(self, painter: &Painter) {
if !self.mesh.is_empty() {
painter.add(Shape::mesh(self.mesh));
}
}
}
pub(crate) fn draw_placeholder(painter: &Painter, polygon: Vec<Pos2>, color: Color32) {
painter.add(egui::epaint::PathShape::convex_polygon(
polygon,
color,
egui::Stroke::NONE,
));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_square_model_is_rasterized_at_the_usual_size() {
assert_eq!(raster_size(1.0), (96, 96));
}
#[test]
fn a_long_thin_model_keeps_a_visible_short_edge() {
let (width, height) = raster_size(117.0);
assert!(height >= 12, "{width} × {height}");
assert_eq!(raster_size(1.0 / 117.0), (height, width));
}
#[test]
fn models_not_drawn_for_a_while_are_forgotten() {
let ctx = Context::default();
let model = Model {
name: "led".to_string(),
size_m: [0.01, 0.01],
svg: String::new(),
};
let mut textures = ModelTextures::default();
for index in 0..=KEPT_MODELS {
textures.texture(&ctx, (None, index, 0, 0), &model);
}
for _ in 0..=IDLE_FRAMES {
textures.begin_frame();
textures.texture(&ctx, (None, 0, 0, 0), &model);
}
textures.begin_frame();
assert_eq!(textures.textures.len(), 1);
assert!(textures.textures.contains_key(&(None, 0, 0, 0)));
}
#[test]
fn a_very_long_model_stops_growing() {
let (width, height) = raster_size(10_000.0);
assert_eq!(width, 2048);
assert!(height >= 1);
}
}