scena 1.7.1

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use base64::Engine;

use super::environment::{DEFAULT_ENVIRONMENT_SOURCE_PATH, is_equirectangular_hdr_path};
use super::environment_sidecar::{EnvironmentPrefilterSidecar, sidecar_path_for_environment};
use super::{AssetFetcher, AssetPath, Assets, EnvironmentDesc, EnvironmentHandle};
use crate::diagnostics::AssetError;

impl<F> Assets<F> {
    pub async fn load_environment(
        &self,
        path: impl Into<AssetPath>,
    ) -> Result<EnvironmentHandle, AssetError>
    where
        F: AssetFetcher,
    {
        let path = path.into();
        if let Some(handle) = self.storage().environment_lookup.get(&path).copied() {
            return Ok(handle);
        }
        let sidecar = if is_equirectangular_hdr_path(&path) {
            self.try_load_environment_sidecar(&path).await?
        } else {
            None
        };
        let environment = if path.as_str() == DEFAULT_ENVIRONMENT_SOURCE_PATH {
            EnvironmentDesc::neutral_studio()
        } else if is_equirectangular_hdr_path(&path) {
            if let Some(source_bytes) = embedded_environment_bytes(&path)? {
                environment_from_hdr_bytes(path.clone(), &source_bytes, sidecar)?
            } else {
                match self.fetcher().fetch(&path).await {
                    Ok(source_bytes) => {
                        environment_from_hdr_bytes(path.clone(), &source_bytes, sidecar)?
                    }
                    Err(AssetError::NotFound { .. } | AssetError::Io { .. }) => {
                        EnvironmentDesc::from_equirectangular_hdr_path(path.clone())
                    }
                    Err(error) => return Err(error),
                }
            }
        } else {
            return Err(AssetError::UnsupportedEnvironmentFormat {
                path: path.as_str().to_string(),
                help: "use Radiance .hdr equirectangular input for the M2 environment path",
            });
        };
        Ok(self.insert_environment(environment))
    }

    pub fn environment(&self, handle: EnvironmentHandle) -> Option<EnvironmentDesc> {
        self.storage().environments.get(handle).cloned()
    }

    pub fn try_environment(
        &self,
        handle: EnvironmentHandle,
    ) -> Result<EnvironmentDesc, AssetError> {
        self.environment(handle)
            .ok_or(AssetError::EnvironmentHandleNotFound {
                environment: handle,
            })
    }

    pub(super) fn insert_environment(&self, environment: EnvironmentDesc) -> EnvironmentHandle {
        let cache_key = environment.source_path().clone();
        let mut storage = self.storage();
        if let Some(handle) = storage.environment_lookup.get(&cache_key) {
            return *handle;
        }
        let handle = storage.environments.insert(environment);
        storage.environment_lookup.insert(cache_key, handle);
        handle
    }

    async fn try_load_environment_sidecar(
        &self,
        environment_path: &AssetPath,
    ) -> Result<Option<EnvironmentPrefilterSidecar>, AssetError>
    where
        F: AssetFetcher,
    {
        if environment_path.as_str().starts_with("data:") {
            return Ok(None);
        }
        let sidecar_path = sidecar_path_for_environment(environment_path);
        match self.fetcher().fetch(&sidecar_path).await {
            Ok(bytes) => match EnvironmentPrefilterSidecar::parse(sidecar_path.clone(), &bytes) {
                Ok(sidecar) => Ok(Some(sidecar)),
                Err(error) => {
                    warn_optional_environment_sidecar_failed(&sidecar_path, &format!("{error:?}"));
                    Ok(None)
                }
            },
            Err(AssetError::NotFound { .. } | AssetError::Io { .. }) => Ok(None),
            Err(error) => Err(error),
        }
    }
}

fn environment_from_hdr_bytes(
    path: AssetPath,
    source_bytes: &[u8],
    sidecar: Option<EnvironmentPrefilterSidecar>,
) -> Result<EnvironmentDesc, AssetError> {
    if let Some(sidecar) = sidecar
        && let Some(environment) = EnvironmentDesc::from_equirectangular_hdr_sidecar_bytes(
            path.clone(),
            source_bytes,
            sidecar,
        )?
    {
        return Ok(environment);
    }
    EnvironmentDesc::from_equirectangular_hdr_bytes(path, source_bytes)
}

fn warn_optional_environment_sidecar_failed(path: &AssetPath, reason: &str) {
    #[cfg(target_arch = "wasm32")]
    {
        web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(&format!(
            "scena asset warning: optional environment sidecar failed for '{}': {}",
            path.as_str(),
            reason
        )));
    }
    #[cfg(not(target_arch = "wasm32"))]
    {
        let _ = (path, reason);
    }
}

fn embedded_environment_bytes(path: &AssetPath) -> Result<Option<Vec<u8>>, AssetError> {
    if !path.as_str().starts_with("data:") {
        return Ok(None);
    }
    let Some((_, encoded)) = path.as_str().split_once(";base64,") else {
        return Err(AssetError::Parse {
            path: path.as_str().to_string(),
            reason:
                "only base64 Radiance HDR data URIs are supported for embedded environment decoding"
                    .to_string(),
        });
    };
    let encoded = encoded
        .split_once('#')
        .map_or(encoded, |(payload, _fragment)| payload);
    let encoded = encoded
        .split_once('?')
        .map_or(encoded, |(payload, _query)| payload);
    base64::engine::general_purpose::STANDARD
        .decode(encoded)
        .map(Some)
        .map_err(|error| AssetError::Parse {
            path: path.as_str().to_string(),
            reason: format!("invalid embedded environment base64: {error}"),
        })
}