115 lines
5.5 KiB
Rust
115 lines
5.5 KiB
Rust
//! A portable picture from warm display meshes only; never evaluate on save.
|
|
use crate::Session;
|
|
use solidcraft_geom::Aabb3;
|
|
use solidcraft_render::{Camera, Rgb, Scene, SceneMesh, StandardView};
|
|
|
|
pub(super) fn saved(s: &Session) -> Option<Vec<u8>> {
|
|
let (scene, camera) = scene(s)?;
|
|
let [width, height] = solidcraft_io::THUMBNAIL_SIZE;
|
|
// Drawn by the renderer the host installed (the GPU); none installed, no thumbnail.
|
|
let renderer = crate::capture::installed()?;
|
|
crate::capture::png_with(Some(&*renderer), &scene, &camera, u32::try_from(width).ok()?, u32::try_from(height).ok()?).ok().map(|p| p.bytes)
|
|
}
|
|
|
|
/// What the thumbnail shows: the shown bodies' warm display meshes in their colours, iso, fitted.
|
|
fn scene(s: &Session) -> Option<(Scene, Camera)> {
|
|
if s.active_sketch.is_some() || !s.pending_moves.is_empty() || s.model.results.iter().any(|r| r.error.is_some()) {
|
|
return None;
|
|
}
|
|
let world = s.cached_world_state()?;
|
|
if world.bodies.is_empty() || world.bodies.len() > 1024 {
|
|
return None;
|
|
}
|
|
let mut scene = Scene::default();
|
|
let mut bounds = Aabb3::EMPTY;
|
|
let mut triangles = 0usize;
|
|
let mut points = 0usize;
|
|
for body in world.bodies.iter().filter(|b| s.body_visible(&b.name)) {
|
|
let mesh = body.cached_mesh()?;
|
|
triangles = triangles.saturating_add(mesh.triangles.len());
|
|
points = points.saturating_add(mesh.positions.len());
|
|
if triangles > 100_000 || points > 100_000 {
|
|
return None;
|
|
}
|
|
bounds = bounds.union(&mesh.bounds());
|
|
let channel = |x: f32| (x.clamp(0., 1.) * 255.).round() as u8;
|
|
let color = s.doc.body_color(body).map_or(crate::view::colors::BODY, |[r, g, b]| Rgb(channel(r), channel(g), channel(b)));
|
|
let face_colors =
|
|
s.doc.face_colors(body).into_iter().map(|(face, look)| (face as u32, Rgb(look.color[0], look.color[1], look.color[2]))).collect();
|
|
scene.meshes.push(SceneMesh { mesh, color, face_colors });
|
|
}
|
|
if bounds.is_empty() {
|
|
return None;
|
|
}
|
|
let mut camera = Camera::default();
|
|
camera.set_view(StandardView::Iso);
|
|
camera.fit(&bounds);
|
|
camera.distance *= 0.7;
|
|
scene.radius = (bounds.diagonal() * 0.5).max(camera.half_height()).max(10.);
|
|
scene.background = Some((crate::view::colors::BG_TOP, crate::view::colors::BG_BOTTOM));
|
|
Some((scene, camera))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn portable_thumbnail_preserves_saved_visibility() {
|
|
crate::capture::install_test_renderer();
|
|
let mut session = Session::default();
|
|
session.execute("solid.box", &serde_json::json!({"length":10,"width":20,"height":30,"body_name":"Visible"})).unwrap();
|
|
session.execute("solid.box", &serde_json::json!({"length":100,"width":100,"height":100,"body_name":"Hidden"})).unwrap();
|
|
session.execute("browser.visibility", &serde_json::json!({"items":["Hidden"],"visible":false})).unwrap();
|
|
let world = session.world_state();
|
|
for body in &world.bodies {
|
|
body.mesh();
|
|
}
|
|
assert_eq!(scene(&session).unwrap().0.meshes.len(), 1, "the hidden body is not in the picture");
|
|
let expected = saved(&session).unwrap();
|
|
let bytes = session.design_container(solidcraft_io::DesignCompression::Fast, false, true).unwrap();
|
|
let doc = solidcraft_io::read_design(&bytes).unwrap();
|
|
assert_eq!(doc.visibility.hidden_bodies, vec!["Hidden"]);
|
|
assert_eq!(doc, *session.design());
|
|
assert_eq!(solidcraft_io::read_design_thumbnail(&bytes), Some(expected));
|
|
let reopened = Session::new(doc);
|
|
assert_eq!(reopened.visibility, session.visibility);
|
|
let disabled = session.design_container(solidcraft_io::DesignCompression::Fast, false, false).unwrap();
|
|
assert_eq!(solidcraft_io::read_design(&disabled).unwrap().visibility, session.visibility);
|
|
assert!(solidcraft_io::read_design_thumbnail(&disabled).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn portable_thumbnail_never_forces_cold_evaluation_or_meshing() {
|
|
crate::capture::install_test_renderer();
|
|
let mut session = Session::default();
|
|
session.execute("solid.box", &serde_json::json!({"length":10,"width":20,"height":30})).unwrap();
|
|
let world = session.world_state();
|
|
assert!(world.bodies.iter().all(|b| b.cached_mesh().is_none()));
|
|
assert!(saved(&session).is_none());
|
|
assert!(world.bodies.iter().all(|b| b.cached_mesh().is_none()));
|
|
for body in &world.bodies {
|
|
body.mesh();
|
|
}
|
|
let png = saved(&session).unwrap();
|
|
let bytes = session.design_container(solidcraft_io::DesignCompression::Fast, false, true).unwrap();
|
|
assert_eq!(solidcraft_io::read_design_thumbnail(&bytes), Some(png));
|
|
assert_eq!(solidcraft_io::read_design(&bytes).unwrap(), *session.doc);
|
|
let disabled = session.design_container(solidcraft_io::DesignCompression::Fast, false, false).unwrap();
|
|
assert!(solidcraft_io::read_design_thumbnail(&disabled).is_none());
|
|
}
|
|
|
|
/// Without a renderer installed (a browser, a tool that draws nothing) a save has no
|
|
/// thumbnail and does not fail.
|
|
#[test]
|
|
fn without_a_renderer_a_save_has_no_thumbnail() {
|
|
let mut session = Session::default();
|
|
session.execute("solid.box", &serde_json::json!({"length":10,"width":20,"height":30})).unwrap();
|
|
for body in &session.world_state().bodies {
|
|
body.mesh();
|
|
}
|
|
let (scene, camera) = scene(&session).unwrap();
|
|
assert!(crate::capture::png_with(None, &scene, &camera, 240, 150).is_err());
|
|
}
|
|
}
|