scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use std::error::Error as StdError;
use std::fmt;

use crate::assets::AssetCatalogAssetV1;
use crate::capture::fnv1a64_hex;
use crate::material::Color;

use super::{ViewerPngError, headless_gltf_viewer};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssetCatalogPreviewPng {
    pub asset_id: String,
    pub source: String,
    pub width: u32,
    pub height: u32,
    pub png_fnv1a64: String,
    pub png_bytes: Vec<u8>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AssetCatalogPreviewError {
    MissingPreview,
    UnsupportedPreviewKind {
        kind: String,
    },
    InvalidGeneratedPreviewSize {
        width: Option<u32>,
        height: Option<u32>,
    },
    Render {
        reason: String,
    },
}

pub async fn render_asset_catalog_preview_png(
    asset: &AssetCatalogAssetV1,
) -> Result<AssetCatalogPreviewPng, AssetCatalogPreviewError> {
    let preview = asset
        .preview
        .as_ref()
        .ok_or(AssetCatalogPreviewError::MissingPreview)?;
    if preview.kind != "generated" {
        return Err(AssetCatalogPreviewError::UnsupportedPreviewKind {
            kind: preview.kind.clone(),
        });
    }
    let (Some(width), Some(height)) = (
        positive_dimension(preview.width),
        positive_dimension(preview.height),
    ) else {
        return Err(AssetCatalogPreviewError::InvalidGeneratedPreviewSize {
            width: preview.width,
            height: preview.height,
        });
    };

    let mut builder = headless_gltf_viewer(asset.source.as_str())
        .size(width, height)
        .with_default_light();
    if let Some(background) = preview.background {
        builder = builder.with_background_color(Color::from_linear_rgba(
            background[0],
            background[1],
            background[2],
            background[3],
        ));
    }

    let png_bytes = builder
        .render_png_bytes()
        .await
        .map_err(AssetCatalogPreviewError::from)?;
    let png_fnv1a64 = fnv1a64_hex(&png_bytes);
    Ok(AssetCatalogPreviewPng {
        asset_id: asset.id.clone(),
        source: asset.source.clone(),
        width,
        height,
        png_fnv1a64,
        png_bytes,
    })
}

fn positive_dimension(value: Option<u32>) -> Option<u32> {
    value.filter(|dimension| *dimension > 0)
}

impl fmt::Display for AssetCatalogPreviewError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MissingPreview => {
                write!(formatter, "catalog asset does not declare preview metadata")
            }
            Self::UnsupportedPreviewKind { kind } => write!(
                formatter,
                "catalog preview kind '{kind}' cannot be generated; use kind 'generated'"
            ),
            Self::InvalidGeneratedPreviewSize { width, height } => write!(
                formatter,
                "generated catalog preview requires positive width and height; got width={width:?}, height={height:?}"
            ),
            Self::Render { reason } => write!(formatter, "catalog preview render failed: {reason}"),
        }
    }
}

impl StdError for AssetCatalogPreviewError {}

impl From<ViewerPngError> for AssetCatalogPreviewError {
    fn from(error: ViewerPngError) -> Self {
        Self::Render {
            reason: error.to_string(),
        }
    }
}