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

src/thumbnail.rs (7.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_format();
 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     // The image the top level shows stands in the scene as it does in the
 57     // viewport, at its physical size in the project's world unit.
 58     let page = crate::page::displayed_page(&proj.root, &proj.root);
 59     let corners = page.as_ref().map(|page| {
 60         crate::page::PageShown {
 61             node_id: String::new(),
 62             size: page.size,
 63             pixels: (page.width, page.height),
 64             origin: page.origin,
 65         }
 66         .world_corners(world_unit_mm(&proj))
 67     });
 68     let rgba = page.as_ref().map(|page| page.to_rgba8());
 69 
 70     let (eye, center, near, far) = view_of(&tris, corners);
 71     let proj_m = Mat4::perspective_rh(FOV, 1.0, near, far);
 72     let view_m = Mat4::look_at_rh(eye, center, Vec3::Y);
 73     let camera =
 74         cce_ui::vk::RtCamera { inv_mvp: (proj_m * view_m).inverse().to_cols_array_2d() };
 75 
 76     let mut off = cce_ui::vk::RtOffscreen::new();
 77     let image = match (&page, &rgba, corners) {
 78         (Some(page), Some(rgba), Some(corners)) => Some(cce_ui::vk::RtImagePixels {
 79             pixels: rgba,
 80             width: page.width,
 81             height: page.height,
 82             corners,
 83             opacity: 1.0,
 84         }),
 85         _ => None,
 86     };
 87     off.set_scene_with_image(&tris, &mats, image);
 88     // The project's Environment node, at the frame rendered.
 89     off.set_environment(crate::environment::Environment::of_scene(&proj.root, frame.unwrap_or(0)).to_rt());
 90     // Behind the scene, the Background Color the project was saved with, as
 91     // the traced viewport shows it; the sky, which still lights the scene,
 92     // for a save without display settings.
 93     off.set_background(
 94         proj.view_state
 95             .display
 96             .as_ref()
 97             .map(|d| cce_ui::colors::to_linear_rgb(d.viewport.bg_color)),
 98     );
 99     let pixels = off.render(camera, size, size, samples.unwrap_or(SAMPLES));
100 
101     let file = std::fs::File::create(out).map_err(|e| format!("create {}: {e}", out.display()))?;
102     let mut encoder = png::Encoder::new(std::io::BufWriter::new(file), size, size);
103     encoder.set_color(png::ColorType::Rgba);
104     encoder.set_depth(png::BitDepth::Eight);
105     crate::page::mark_srgb(&mut encoder);
106     let mut writer = encoder.write_header().map_err(|e| format!("png header: {e}"))?;
107     writer.write_image_data(&pixels).map_err(|e| format!("png write: {e}"))?;
108     writer.finish().map_err(|e| format!("png finish: {e}"))?;
109     Ok(())
110 }
111 
112 /// The thumbnail camera's vertical field of view.
113 const FOV: f32 = 0.9;
114 
115 /// One world unit of the project in millimetres: the unit its display
116 /// settings name, or the app's own default for a save from before it
117 /// carried them.
118 fn world_unit_mm(proj: &Project) -> f32 {
119     let name = proj
120         .view_state
121         .display
122         .as_ref()
123         .map(|d| d.viewport.world_unit.clone())
124         .unwrap_or_else(|| crate::app::ViewportSettings::default().world_unit);
125     let unit = cce_ui::units::Unit::parse(&name).unwrap_or(cce_ui::units::Unit::Mm);
126     let metric = cce_ui::units::metric();
127     cce_ui::units::Len::new(1.0, unit).convert(cce_ui::units::Unit::Mm, &metric).value
128 }
129 
130 /// Where the thumbnail is taken from: the eye, what it looks at, and the
131 /// near and far planes.
132 ///
133 /// Geometry is seen from a pleasant high diagonal, its bounding sphere fitted
134 /// to the view, and an image standing with it is inside that sphere. An
135 /// image ALONE is seen square on and fitted edge to edge: from the diagonal
136 /// a picture is a slanted sliver of itself, and the thumbnail of a picture
137 /// is the picture. An empty scene still renders (sky).
138 pub(crate) fn view_of(
139     tris: &[cce_ui::vk::RtTriangle],
140     image: Option<[[f32; 3]; 4]>,
141 ) -> (Vec3, Vec3, f32, f32) {
142     if let (true, Some(corners)) = (tris.is_empty(), image) {
143         let [tl, tr, br, _] = corners.map(Vec3::from_array);
144         let center = (tl + br) * 0.5;
145         let longest = (tr - tl).length().max((br - tr).length()).max(1e-3);
146         let dist = longest / (2.0 * (FOV * 0.5).tan()) * 1.05;
147         return (center + Vec3::Z * dist, center, dist * 0.01, dist * 4.0);
148     }
149     let points = tris
150         .iter()
151         .flat_map(|t| [t.p0, t.p1, t.p2])
152         .chain(image.into_iter().flatten())
153         .map(Vec3::from_array);
154     let (center, radius) = bounds(points);
155     let dist = (radius / (FOV * 0.5).sin()).max(0.5) * 1.15;
156     let eye = center + Vec3::new(1.0, 0.65, 1.0).normalize() * dist;
157     let near = (dist - radius * 2.0).max(dist * 0.01);
158     let far = dist + radius * 4.0 + 1.0;
159     (eye, center, near, far)
160 }
161 
162 fn bounds(points: impl Iterator<Item = Vec3>) -> (Vec3, f32) {
163     let mut min = Vec3::splat(f32::INFINITY);
164     let mut max = Vec3::splat(f32::NEG_INFINITY);
165     for v in points {
166         min = min.min(v);
167         max = max.max(v);
168     }
169     if !min.is_finite() {
170         return (Vec3::ZERO, 1.0);
171     }
172     let center = (min + max) * 0.5;
173     let radius = (max - center).length().max(1e-3);
174     (center, radius)
175 }