//! Actual production pass order and GUI state. The bounded worker/mailbox and //! GPU service are implemented in `production`; previews retain native size. use crate::preview::{paperdoll_yaws_deg, PAPERDOLL_PITCHES_DEG}; pub use crate::production::{spawn, spawn_resume, RenderJob, RenderThread}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PassKind { HighresImage, DetailMap, TargetResolution, Depth, Normal, Specular, } impl PassKind { pub fn label(self) -> &'static str { match self { Self::HighresImage => "highres", Self::DetailMap => "detail", Self::TargetResolution => "doelresolutie", Self::Depth => "depth", Self::Normal => "normal", Self::Specular => "specular", } } pub fn produces_image(self) -> bool { true } } pub const PASS_ORDER: [PassKind; 6] = [ PassKind::HighresImage, PassKind::DetailMap, PassKind::TargetResolution, PassKind::Depth, PassKind::Normal, PassKind::Specular, ]; #[derive(Debug, Clone, Copy, PartialEq)] pub struct ViewAngle { pub pitch_deg: f32, pub yaw_deg: f32, } #[derive(Debug, Clone, Copy, PartialEq)] pub struct PassStep { pub pitch_deg: f32, pub yaw_deg: f32, pub kind: PassKind, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ImageBuf { pub width: u32, pub height: u32, pub rgba: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Progress { pub completed: u32, pub total: u32, pub label: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum RenderEvent { Step { index: u32, total: u32, label: String, }, Pass { index: u32, total: u32, label: String, image: Option, }, Failed(String), Cancelled, Finished { saved: Option, }, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RenderUiState { pub progress: Progress, pub preview: Option, pub preview_label: String, pub preview_revision: u64, pub saved: Option, pub error: Option, pub running: bool, /// Every warning step of this render (e.g. clamped depth per view), /// kept so a later pass label cannot hide it. Capped at `MAX_WARNINGS`. pub warnings: Vec, } pub const MAX_WARNINGS: usize = 100_000; impl RenderUiState { pub fn idle() -> Self { Self { progress: Progress { completed: 0, total: 0, label: String::new(), }, preview: None, preview_label: String::new(), preview_revision: 0, saved: None, error: None, running: false, warnings: Vec::new(), } } pub fn apply(&mut self, event: RenderEvent) { match event { RenderEvent::Step { index, total, label, } => { if label.starts_with("Waarschuwing") && self.warnings.len() < MAX_WARNINGS { self.warnings.push(label.clone()); } self.progress = Progress { completed: index, total, label, }; self.running = true; } RenderEvent::Pass { index, total, label, image, } => { self.progress = Progress { completed: index + 1, total, label, }; if let Some(image) = image { self.preview_label = self.progress.label.clone(); self.preview_revision = self.preview_revision.wrapping_add(1); self.preview = Some(image); } self.running = true; } RenderEvent::Cancelled => { self.running = false; self.progress.label = "Geannuleerd; gedeeltelijke uitvoer behouden".into(); } RenderEvent::Failed(message) => { self.error = Some(message); self.running = false; } RenderEvent::Finished { saved } => { self.saved = saved; self.running = false; if self.progress.total > 0 { self.progress.completed = self.progress.total; } } } } } pub fn export_views() -> Vec { let mut views = Vec::with_capacity(PAPERDOLL_PITCHES_DEG.len() * paperdoll_yaws_deg().len()); for pitch in PAPERDOLL_PITCHES_DEG { for yaw in paperdoll_yaws_deg() { views.push(ViewAngle { pitch_deg: pitch, yaw_deg: yaw, }); } } views } pub fn pass_sequence(views: &[ViewAngle]) -> Vec { let mut steps = Vec::with_capacity(views.len() * PASS_ORDER.len()); for view in views { for kind in PASS_ORDER { steps.push(PassStep { pitch_deg: view.pitch_deg, yaw_deg: view.yaw_deg, kind, }); } } steps } /// Nearest-neighbor stretch. Every destination pixel is filled, so the /// preview box has no empty border. pub fn nearest_stretch(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec { let mut out = vec![0u8; (dw as usize) * (dh as usize) * 4]; if sw == 0 || sh == 0 || dw == 0 || dh == 0 { return out; } for y in 0..dh { let sy = ((y as u64 * sh as u64) / dh as u64) as u32; let sy = sy.min(sh - 1); for x in 0..dw { let sx = ((x as u64 * sw as u64) / dw as u64) as u32; let sx = sx.min(sw - 1); let from = ((sy * sw + sx) * 4) as usize; let to = ((y * dw + x) * 4) as usize; out[to..to + 4].copy_from_slice(&src[from..from + 4]); } } out } #[derive(Debug, Clone, Copy, PartialEq)] pub struct PreviewFit { pub size: [f32; 2], pub offset: [f32; 2], } pub fn fit_preview(image: [u32; 2], bounds: [f32; 2]) -> PreviewFit { if image.contains(&0) || bounds.iter().any(|v| !v.is_finite() || *v <= 0.0) { return PreviewFit { size: [0.; 2], offset: [0.; 2], }; } let scale = (bounds[0] / image[0] as f32).min(bounds[1] / image[1] as f32); let size = [image[0] as f32 * scale, image[1] as f32 * scale]; PreviewFit { size, offset: [(bounds[0] - size[0]) * 0.5, (bounds[1] - size[1]) * 0.5], } } #[cfg(test)] mod tests { use super::*; #[test] fn full_view_order_is_pitch_then_yaw_then_passes() { let views = export_views(); assert_eq!(views.len(), 60); assert_eq!(views[0].pitch_deg, -60.0); assert_eq!(views[0].yaw_deg, 0.0); assert_eq!(views[11].yaw_deg, 330.0); assert_eq!(views[12].pitch_deg, -30.0); assert_eq!(views[24].pitch_deg, 0.0); assert_eq!(views[59].pitch_deg, 60.0); let steps = pass_sequence(&views); assert_eq!(steps.len(), 60 * 6); let first: Vec<_> = steps.iter().take(6).map(|step| step.kind).collect(); assert_eq!(first, PASS_ORDER.to_vec()); assert_eq!(steps[6].yaw_deg, 30.0); assert_eq!(steps[6].kind, PassKind::HighresImage); } #[test] fn preview_uniform_scale_centers_and_letterboxes_all_aspects() { let square = fit_preview([128, 128], [480., 640.]); assert_eq!(square.size, [480., 480.]); assert_eq!(square.offset, [0., 80.]); let landscape = fit_preview([640, 320], [300., 300.]); assert_eq!(landscape.size, [300., 150.]); assert_eq!(landscape.offset, [0., 75.]); let portrait = fit_preview([64, 128], [400., 200.]); assert_eq!(portrait.size, [100., 200.]); assert_eq!(portrait.offset, [150., 0.]); assert_eq!(fit_preview([0, 128], [400., 200.]).size, [0., 0.]); assert_eq!(fit_preview([128, 128], [f32::NAN, 200.]).size, [0., 0.]); } #[test] fn nearest_non_square_uses_source_height_for_rows() { let src = [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]; let out = nearest_stretch(&src, 1, 3, 2, 6); for y in 0..6 { assert_eq!(&out[y * 8..y * 8 + 4], &src[(y / 2) * 4..(y / 2) * 4 + 4]); } } #[test] fn nearest_stretch_fills_the_box() { let src = [ 255, 0, 0, 255, 0, 0, 255, 255, // 2x1 ]; let out = nearest_stretch(&src, 2, 1, 4, 3); assert_eq!(out.len(), 4 * 3 * 4); for y in 0..3 { for x in 0..4 { let pixel = &out[(y * 4 + x) * 4..(y * 4 + x) * 4 + 4]; if x < 2 { assert_eq!(pixel, &[255, 0, 0, 255]); } else { assert_eq!(pixel, &[0, 0, 255, 255]); } } } } #[test] fn pass_progress_cancellation_and_last_image_are_preserved() { let mut ui = RenderUiState::idle(); ui.apply(RenderEvent::Pass { index: 1, total: 6, label: "detail".into(), image: Some(ImageBuf { width: 1, height: 1, rgba: vec![1, 2, 3, 255], }), }); ui.apply(RenderEvent::Step { index: 2, total: 6, label: "target".into(), }); assert_eq!(ui.preview.as_ref().unwrap().rgba, vec![1, 2, 3, 255]); assert_eq!(ui.preview_revision, 1); assert_eq!(ui.preview_label, "detail"); assert_eq!(ui.progress.label, "target"); ui.apply(RenderEvent::Cancelled); assert!(!ui.running); assert_eq!(ui.progress.completed, 2); assert_eq!(ui.preview_revision, 1); assert_eq!(ui.preview.as_ref().unwrap().rgba, vec![1, 2, 3, 255]); } #[test] fn warning_steps_are_kept_after_later_passes() { let mut ui = RenderUiState::idle(); for label in [ "Waarschuwing: x ยท 3 highres-pixels buiten dieptebereik (geklemd)", "target", ] { ui.apply(RenderEvent::Step { index: 1, total: 2, label: label.into(), }); } assert_eq!(ui.progress.label, "target"); assert_eq!(ui.warnings.len(), 1); } }