Skip to main content

scena/viewer/
asset_catalog_preview.rs

1use 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}