use super::{gallery_design, set_thumbnail};
use crate::{
MainWindow,
gui::{
latest_worker::LatestWorker,
render::camera_lighting::fit_distance_for_radius,
solid_preview::{
mesh_cache::MeshCache,
raster::{SolidRasterizer, SolidStyle},
to_pixel_buffer,
},
},
};
use glam::Vec3;
use indicatrix::optics::raytracer::{Camera, DEFAULT_FOV_DEG};
use indicatrix_cut_core::Design;
use indicatrix_editor::solve_policy::design_to_gpu_planes_from_solved;
use slint::{Rgba8Pixel, SharedPixelBuffer, Weak};
use std::{
collections::HashMap,
sync::{Arc, Mutex, PoisonError},
};
use tracing::warn;
pub(super) type Pixels = SharedPixelBuffer<Rgba8Pixel>;
pub(super) const THUMBNAIL_EDGE: u32 = 160;
const THUMBNAIL_YAW: f32 = 0.6;
const THUMBNAIL_PITCH: f32 = 0.85;
#[must_use]
pub(super) fn render_thumbnail(design: &Design) -> Option<Pixels> {
let solved = design.solve().ok()?;
let planes: Vec<(Vec3, f32)> = design_to_gpu_planes_from_solved(design, &solved)
.iter()
.map(|plane| (Vec3::from(plane.normal), -plane.d))
.collect();
let mut cache = MeshCache::default();
let mesh = cache.get_or_build(&planes)?;
let camera = Camera::new(
THUMBNAIL_YAW,
THUMBNAIL_PITCH,
fit_distance_for_radius(mesh.bounding_radius()),
DEFAULT_FOV_DEG,
);
let style = SolidStyle {
show_orientation_marker: false,
show_preform: false,
preform_plane_count: design.preform.planes().len(),
..SolidStyle::default()
};
let mut raster = SolidRasterizer::new(THUMBNAIL_EDGE, THUMBNAIL_EDGE);
raster.render_prepared(mesh, &camera, &style);
Some(to_pixel_buffer(&raster))
}
fn picture(cache: &Mutex<HashMap<i32, Pixels>>, template_index: i32) -> Option<Pixels> {
let cached = cache
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get(&template_index)
.cloned();
if cached.is_some() {
return cached;
}
let pixels = render_thumbnail(&gallery_design(template_index))?;
cache
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert(template_index, pixels.clone());
Some(pixels)
}
pub(super) struct Thumbnails {
worker: LatestWorker<Vec<i32>>,
}
impl Thumbnails {
pub(super) fn spawn(ui_weak: Weak<MainWindow>) -> Self {
let cache: Arc<Mutex<HashMap<i32, Pixels>>> = Arc::default();
let worker = LatestWorker::spawn_with_panic_hook(
"template-thumbnails",
move |wanted: Vec<i32>| {
for template_index in wanted {
let Some(pixels) = picture(&cache, template_index) else {
warn!("template {template_index} did not draw a thumbnail");
continue;
};
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
set_thumbnail(&ui, template_index, &pixels);
});
}
},
|| {},
);
Self { worker }
}
pub(super) fn request(&self, wanted: Vec<i32>) {
if !wanted.is_empty() && !self.worker.submit(wanted) {
warn!("the template thumbnail worker is not running");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix_editor::templates::gallery_cards;
use std::collections::BTreeSet;
fn painted(pixels: &Pixels) -> usize {
pixels.as_slice().iter().filter(|pixel| pixel.a > 0).count()
}
#[test]
fn every_template_draws_a_stone_on_a_transparent_background() {
let total = (THUMBNAIL_EDGE * THUMBNAIL_EDGE) as usize;
for card in gallery_cards()
.into_iter()
.filter(|card| card.template_index >= 1)
{
let pixels = render_thumbnail(&gallery_design(card.template_index))
.unwrap_or_else(|| panic!("{} did not draw", card.name));
assert_eq!(pixels.width(), THUMBNAIL_EDGE, "{}", card.name);
assert_eq!(pixels.height(), THUMBNAIL_EDGE, "{}", card.name);
let painted = painted(&pixels);
assert!(
painted > total / 20,
"{}: only {painted} of {total} pixels are painted",
card.name
);
assert!(
painted < total * 9 / 10,
"{}: {painted} of {total} pixels are painted -- the stone fills the frame",
card.name
);
assert_eq!(
pixels.as_slice()[0].a,
0,
"{}: the corner is not transparent",
card.name
);
}
}
#[test]
fn the_featured_shapes_draw_different_pictures() {
let counts: BTreeSet<usize> = gallery_cards()
.into_iter()
.filter(|card| card.template_index >= 1 && card.group.code() == 1)
.map(|card| {
painted(
&render_thumbnail(&gallery_design(card.template_index))
.expect("a featured shape draws"),
)
})
.collect();
assert!(counts.len() >= 4, "painted-pixel counts: {counts:?}");
}
#[test]
fn the_blank_design_still_draws_without_failing() {
assert!(render_thumbnail(&gallery_design(0)).is_some());
}
}