git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

src/thumbnail.rs (4.7K)

  1 //! `cce-designer --thumbnail <project> <out.png> [--size N]` — headless
  2 //! path-traced project thumbnails (RT-renderer phase 3).
  3 //!
  4 //! Loads a project's `state.json`, regenerates its geometry from the node
  5 //! graph (the same `network_sphere_vertices_with_errors` the viewport uses,
  6 //! wrangles included), auto-frames a camera on the scene bounds, and
  7 //! renders through `cce_ui::vk::RtOffscreen` — no window, no compositor, any
  8 //! graphics-capable Vulkan device. cce-files shells out to this for its
  9 //! preview cache.
 10 
 11 use std::path::Path;
 12 
 13 use glam::{Mat4, Vec3};
 14 
 15 use crate::app::Project;
 16 use crate::geometry::{network_sphere_vertices_with_errors, rt_scene_from_verts};
 17 
 18 const SAMPLES: u32 = 96;
 19 
 20 /// Render `project` (a project directory or a `state.json` path) to a square
 21 /// `size`×`size` PNG at `out`, with `samples` paths per pixel (None = 96).
 22 pub fn run(project: &Path, out: &Path, size: u32, samples: Option<u32>, frame: Option<i32>) -> Result<(), String> {
 23     let state_file = if project.is_dir() { project.join("state.json") } else { project.to_path_buf() };
 24     let content = std::fs::read_to_string(&state_file)
 25         .map_err(|e| format!("read {}: {e}", state_file.display()))?;
 26     let mut proj: Project =
 27         serde_json::from_str(&content).map_err(|e| format!("parse {}: {e}", state_file.display()))?;
 28     // Same template merge the app applies on load, so a thumbnail of an old
 29     // scene shows what opening it would show.
 30     let templates = crate::app::flatten_node_templates(&crate::app::load_fs_tree());
 31     proj.sanitize_node_names();
 32     proj.migrate_param_refs();
 33     crate::app::merge_template_defs(&mut proj.root, &templates);
 34 
 35     let mut ocl_error = None;
 36     // Without `--frame`, a headless thumbnail has no timeline and simnets
 37     // render at their seed. WITH it, the solve runs to that frame — which is
 38     // the only way to look at a simulation without a Wayland session, and so
 39     // the only way to check that a growth chain does what it claims.
 40     //
 41     // The start frame is the playbar's default of 1, the same number the app
 42     // uses, so a frame number here means what it means in the window. A simnet
 43     // with its own Start Frame answers for itself either way.
 44     let mut sim_cache = crate::geometry::SimCache::default();
 45     let mut sim = crate::geometry::EvalSim::new(frame.unwrap_or(0), 1, &mut sim_cache);
 46     // Thumbnails always show the whole scene from the top, regardless of the
 47     // network level the project was saved at: root as both eval and walk root.
 48     let geom = network_sphere_vertices_with_errors(&proj.root, &proj.root, &mut ocl_error, &mut sim);
 49     if let Some(e) = ocl_error {
 50         // Non-fatal: a failing node just contributes nothing, like the viewport.
 51         eprintln!("thumbnail: node error (geometry partially skipped): {e}");
 52     }
 53     let verts = crate::geometry::detail_vertices(&geom);
 54     let (tris, mats) = rt_scene_from_verts(&verts);
 55 
 56     // Frame the scene: bounding sphere fit into a 0.9 rad vertical FOV from a
 57     // pleasant high-diagonal direction. An empty scene still renders (sky).
 58     let (center, radius) = bounds(&tris);
 59     let fov = 0.9f32;
 60     let dist = (radius / (fov * 0.5).sin()).max(0.5) * 1.15;
 61     let eye = center + Vec3::new(1.0, 0.65, 1.0).normalize() * dist;
 62     let near = (dist - radius * 2.0).max(dist * 0.01);
 63     let far = dist + radius * 4.0 + 1.0;
 64     let proj_m = Mat4::perspective_rh(fov, 1.0, near, far);
 65     let view_m = Mat4::look_at_rh(eye, center, Vec3::Y);
 66     let camera =
 67         cce_ui::vk::RtCamera { inv_mvp: (proj_m * view_m).inverse().to_cols_array_2d() };
 68 
 69     let mut off = cce_ui::vk::RtOffscreen::new();
 70     off.set_scene(&tris, &mats);
 71     let pixels = off.render(camera, size, size, samples.unwrap_or(SAMPLES));
 72 
 73     let file = std::fs::File::create(out).map_err(|e| format!("create {}: {e}", out.display()))?;
 74     let mut encoder = png::Encoder::new(std::io::BufWriter::new(file), size, size);
 75     encoder.set_color(png::ColorType::Rgba);
 76     encoder.set_depth(png::BitDepth::Eight);
 77     crate::page::mark_srgb(&mut encoder);
 78     let mut writer = encoder.write_header().map_err(|e| format!("png header: {e}"))?;
 79     writer.write_image_data(&pixels).map_err(|e| format!("png write: {e}"))?;
 80     writer.finish().map_err(|e| format!("png finish: {e}"))?;
 81     Ok(())
 82 }
 83 
 84 fn bounds(tris: &[cce_ui::vk::RtTriangle]) -> (Vec3, f32) {
 85     if tris.is_empty() {
 86         return (Vec3::ZERO, 1.0);
 87     }
 88     let mut min = Vec3::splat(f32::INFINITY);
 89     let mut max = Vec3::splat(f32::NEG_INFINITY);
 90     for t in tris {
 91         for p in [t.p0, t.p1, t.p2] {
 92             let v = Vec3::from_array(p);
 93             min = min.min(v);
 94             max = max.max(v);
 95         }
 96     }
 97     let center = (min + max) * 0.5;
 98     let radius = (max - center).length().max(1e-3);
 99     (center, radius)
100 }