scena/viewer/
asset_catalog_preview.rs1use std::error::Error as StdError;
2use std::fmt;
3
4use crate::assets::AssetCatalogAssetV1;
5use crate::capture::fnv1a64_hex;
6use crate::material::Color;
7
8use super::{ViewerPngError, headless_gltf_viewer};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct AssetCatalogPreviewPng {
12 pub asset_id: String,
13 pub source: String,
14 pub width: u32,
15 pub height: u32,
16 pub png_fnv1a64: String,
17 pub png_bytes: Vec<u8>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum AssetCatalogPreviewError {
22 MissingPreview,
23 UnsupportedPreviewKind {
24 kind: String,
25 },
26 InvalidGeneratedPreviewSize {
27 width: Option<u32>,
28 height: Option<u32>,
29 },
30 Render {
31 reason: String,
32 },
33}
34
35pub async fn render_asset_catalog_preview_png(
36 asset: &AssetCatalogAssetV1,
37) -> Result<AssetCatalogPreviewPng, AssetCatalogPreviewError> {
38 let preview = asset
39 .preview
40 .as_ref()
41 .ok_or(AssetCatalogPreviewError::MissingPreview)?;
42 if preview.kind != "generated" {
43 return Err(AssetCatalogPreviewError::UnsupportedPreviewKind {
44 kind: preview.kind.clone(),
45 });
46 }
47 let (Some(width), Some(height)) = (
48 positive_dimension(preview.width),
49 positive_dimension(preview.height),
50 ) else {
51 return Err(AssetCatalogPreviewError::InvalidGeneratedPreviewSize {
52 width: preview.width,
53 height: preview.height,
54 });
55 };
56
57 let mut builder = headless_gltf_viewer(asset.source.as_str())
58 .size(width, height)
59 .with_default_light();
60 if let Some(background) = preview.background {
61 builder = builder.with_background_color(Color::from_linear_rgba(
62 background[0],
63 background[1],
64 background[2],
65 background[3],
66 ));
67 }
68
69 let png_bytes = builder
70 .render_png_bytes()
71 .await
72 .map_err(AssetCatalogPreviewError::from)?;
73 let png_fnv1a64 = fnv1a64_hex(&png_bytes);
74 Ok(AssetCatalogPreviewPng {
75 asset_id: asset.id.clone(),
76 source: asset.source.clone(),
77 width,
78 height,
79 png_fnv1a64,
80 png_bytes,
81 })
82}
83
84fn positive_dimension(value: Option<u32>) -> Option<u32> {
85 value.filter(|dimension| *dimension > 0)
86}
87
88impl fmt::Display for AssetCatalogPreviewError {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 Self::MissingPreview => {
92 write!(formatter, "catalog asset does not declare preview metadata")
93 }
94 Self::UnsupportedPreviewKind { kind } => write!(
95 formatter,
96 "catalog preview kind '{kind}' cannot be generated; use kind 'generated'"
97 ),
98 Self::InvalidGeneratedPreviewSize { width, height } => write!(
99 formatter,
100 "generated catalog preview requires positive width and height; got width={width:?}, height={height:?}"
101 ),
102 Self::Render { reason } => write!(formatter, "catalog preview render failed: {reason}"),
103 }
104 }
105}
106
107impl StdError for AssetCatalogPreviewError {}
108
109impl From<ViewerPngError> for AssetCatalogPreviewError {
110 fn from(error: ViewerPngError) -> Self {
111 Self::Render {
112 reason: error.to_string(),
113 }
114 }
115}