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 }