//! Native GUI smoke for the rendered 2D preview tab: it loads a synthetic //! export through its `.ppda` + atlases (loose PNGs are deleted first). //! Usage: preview2d_smoke OUTPUT_DIR [EXISTING_PROJECT_DIR] use eframe::App; use paperdoll_renderer::editor::EditorApp; use paperdoll_renderer::export_fixture::{Pixel, SyntheticRun}; use paperdoll_renderer::preview2d::discover_completed_runs; use paperdoll_renderer::render_job::ImageBuf; use paperdoll_renderer::session::Session; use std::fs; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; struct Smoke { editor: EditorApp, out: PathBuf, started: Instant, frames: u64, done: bool, shot_requested: bool, shot_saved: bool, } impl App for Smoke { fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) { self.frames += 1; self.editor.update(ctx, frame); if self.frames == 2 { self.editor.open_preview2d(); } if self.frames == 4 { // Steepest pitch row (+60) and yaw 30 prove the five-row atlas layout. let state = self.editor.preview2d_state_mut(); state.step_pitch(2); state.step_yaw(1); } ctx.request_repaint(); if self.frames >= 30 && !self.shot_saved { if !self.shot_requested { self.shot_requested = true; ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Screenshot); } else if let Some(image) = ctx.input(|i| { i.raw.events.iter().find_map(|e| match e { eframe::egui::Event::Screenshot { image, .. } => Some(image.clone()), _ => None, }) }) { let [w, h] = image.size; let rgba: Vec = image.pixels.iter().flat_map(|p| p.to_array()).collect(); write_png( &self.out.join("preview2d-gui.png"), &ImageBuf { width: w as u32, height: h as u32, rgba, }, ) .expect("png"); self.shot_saved = true; } } let ready = self.editor.preview2d_image().is_some() || self.editor.preview2d_state().diagnostic.is_some(); let timeout = self.started.elapsed() > Duration::from_secs(20); if self.done { return; } if (self.shot_saved && ready) || timeout { self.done = true; let state = self.editor.preview2d_state(); let image_ok = if let Some(img) = self.editor.preview2d_image() { write_png(&self.out.join("preview2d-composed.png"), img).expect("png"); true } else { false }; let value = serde_json::json!({ "success": image_ok && self.shot_saved, "screenshot": self.shot_saved, "gui_updates": self.frames, "sex": state.sex_token(), "clip": state.clip_token, "frame": state.frame_index(), "yaw": state.yaw_deg(), "pitch": state.pitch_deg(), "playing": state.playing, "diagnostic": state.diagnostic, "elapsed_secs": state.elapsed_secs, }); fs::write( self.out.join("gui-smoke.json"), serde_json::to_vec_pretty(&value).unwrap(), ) .unwrap(); println!("RESULT {value}"); ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); } } fn on_exit(&mut self, gl: Option<&glow::Context>) { self.editor.on_exit(gl); } } fn write_png(path: &Path, img: &ImageBuf) -> Result<(), String> { let file = fs::File::create(path).map_err(|e| e.to_string())?; let mut encoder = png::Encoder::new(file, img.width, img.height); encoder.set_color(png::ColorType::Rgba); encoder.set_depth(png::BitDepth::Eight); let mut writer = encoder.write_header().map_err(|e| e.to_string())?; writer .write_image_data(&img.rgba) .map_err(|e| e.to_string()) } /// Body export: 2 frames, every view gets a distinct colour from its /// pitch/yaw, written with real loose PNGs + atlases + .ppda. fn install_body_export(project: &Path) -> Result<(), String> { let mut spec = SyntheticRun::body("smoke-preview2d", "male", b"preview2d-smoke-body"); spec.target = [2, 2]; spec.clips = vec![("walk".into(), 2, 0.5)]; spec.write( &project.join("renders/male-body-atlas"), |_, frame, pitch, yaw| Pixel { diffuse: [ 40u8.wrapping_add(frame as u8 * 80), (pitch + 90) as u8, (yaw / 2) as u8, 255, ], normal: [128, 128, 255, 255], specular: [0, 0, 0, 255], depth: 25000 - frame as u16 * 1000, }, ) } fn main() -> Result<(), Box> { let out = PathBuf::from( std::env::args() .nth(1) .ok_or("usage: preview2d_smoke OUTPUT_DIR")?, ); fs::create_dir_all(&out)?; // Optional second argument: an existing project with real production // exports (its loose PNGs are left alone). let project = match std::env::args().nth(2) { Some(existing) => PathBuf::from(existing), None => { let project = out.join("project"); let mut session = Session::new(); session.install_new_project(&project)?; session.save()?; install_body_export(&project)?; // Prove the preview reads the atlases: remove every loose PNG. fs::remove_dir_all(project.join("renders/male-body-atlas/male"))?; project } }; let catalog = discover_completed_runs(&project); fs::write( out.join("catalog.json"), serde_json::to_vec_pretty(&serde_json::json!({ "count": catalog.len(), "fps": catalog.iter().map(|e| e.fps).collect::>(), "sets": catalog.iter().map(|e| format!("{}:{}", e.sex, e.set)).collect::>(), }))?, )?; let mut editor = EditorApp::new(Box::new(paperdoll_renderer::dialogs::SystemDialogs::new())); editor.open_path(project.join("character.ppd")); eframe::run_native( "Paperdoll Preview2d Smoke", eframe::NativeOptions { viewport: eframe::egui::ViewportBuilder::default() .with_inner_size([1100.0, 720.0]) .with_visible(true), ..Default::default() }, Box::new(move |_cc| { Ok(Box::new(Smoke { editor, out: out.clone(), started: Instant::now(), frames: 0, done: false, shot_requested: false, shot_saved: false, }) as Box) }), ) .map_err(|e| e.to_string())?; Ok(()) }