//! Actual native end-to-end batch smoke; no GUI input automation or preview delays. //! Usage: production_fixture ROOT MALE.glb FEMALE.glb male|female WIDTH HEIGHT FPS [equipment] [--height-milli=N] use eframe::App; use paperdoll_renderer::{ editor::EditorApp, equipment, preview, session::{adopt_file, Session, Sex}, }; use std::path::PathBuf; struct Smoke { editor: EditorApp, root: PathBuf, last: u32, frames: u64, done: 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); let (completed, total, label) = self.editor.render_progress(); if completed != self.last { println!("PASS {completed}/{total}: {label}"); self.last = completed; } if !self.editor.render_is_running() && !self.done { self.done = true; let error = self.editor.render_error(); let success = error.is_none() && total > 0 && completed == total; let value = serde_json::json!({"success":success,"completed":completed,"total":total,"last_pass":label,"gui_updates":self.frames,"error":error}); std::fs::write( self.root.join("smoke-result.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 main() -> Result<(), Box> { let a = std::env::args().skip(1).collect::>(); if a.len() < 7 { return Err( "ROOT MALE FEMALE male|female WIDTH HEIGHT FPS [equipment] [--height-milli=N]".into(), ); } let root = PathBuf::from(&a[0]); let mut s = Session::new(); s.install_new_project(&root)?; for (sex, index) in [(Sex::Male, 1), (Sex::Female, 2)] { let path = adopt_file(&root, &PathBuf::from(&a[index]), "assets/body")?; s.set_body_path(sex, path); } s.set_render(a[4].parse()?, a[5].parse()?, a[6].parse()?); let sex = match a[3].as_str() { "male" => Sex::Male, "female" => Sex::Female, _ => return Err("invalid sex".into()), }; let equipment = a.get(7).is_some_and(|v| v == "equipment"); if let Some(value) = a.iter().find_map(|v| v.strip_prefix("--height-milli=")) { s.opened.as_mut().unwrap().document.render.framing[usize::from(sex == Sex::Female)] .height_milli = value.parse()?; } if equipment { let assets = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/equipment"); let mut p = equipment::Preset::new("sword".into()); p.provenance = "CC0 generated fixture".into(); equipment::import_obj(&mut p, &assets.join("sword.obj"))?; equipment::import_shiny(&mut p, 0, &assets.join("shiny.png"))?; for (i, sex) in [(0, Sex::Male), (1, Sex::Female)] { let b = preview::load_gltf_file(&root.join(s.body_path(sex).unwrap()))?; let joint = b .joint_names .iter() .position(|n| n == "RightHand") .ok_or("RightHand absent")?; p.bind(i, &b, 0, joint)?; } let i = s.add_equipment(); s.equipment[i].slug = "sword".into(); s.equipment[i].preset = Some(p); } s.save()?; for sex in [Sex::Male, Sex::Female] { let b = preview::load_gltf_file(&root.join(s.body_path(sex).unwrap()))?; println!( "PLAN {sex:?}: {:?}", paperdoll_renderer::production::plan(&b, a[6].parse()?)? ); } let root2 = root.clone(); eframe::run_native( "Paperdoll Production Render", eframe::NativeOptions { viewport: eframe::egui::ViewportBuilder::default().with_inner_size([1150., 820.]), ..paperdoll_renderer::preview_gl::native_options() }, Box::new(move |_| { let mut editor = EditorApp::default(); editor.open_path(root2.join("character.ppd")); if equipment { editor.open_equipment_configuration(0, sex); } else { editor.open_body_configuration(sex); } editor.render_all_angles(); Ok(Box::new(Smoke { editor, root: root2, last: 0, frames: 0, done: false, })) }), )?; let report: serde_json::Value = serde_json::from_slice(&std::fs::read(root.join("smoke-result.json"))?)?; if report["success"] != true { return Err("production smoke failed; see smoke-result.json".into()); } Ok(()) }