scena 1.7.0

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

use serde::{Deserialize, Serialize};

use crate::diagnostics::AssetError;

use super::{AssetPath, AssetProvenance, SceneAsset, SceneAssetGeometrySummary};

pub const ASSET_LOAD_REPORT_SCHEMA_V1: &str = "scena.asset_load_report.v1";

#[derive(Debug, Clone)]
pub struct AssetLoadControl {
    cancelled: Arc<AtomicBool>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AssetLoadReport<T> {
    pub(super) asset: T,
    pub(super) path: AssetPath,
    pub(super) cache_hit: bool,
    pub(super) fetched_bytes: usize,
    pub(super) external_buffers: usize,
    pub(super) external_images: usize,
    pub(super) warnings: Vec<AssetLoadWarning>,
    pub(super) progress_events: Vec<AssetLoadProgress>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct AssetLoadOptions {
    strict_textures: bool,
    strict_external_resources: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AssetLoadWarning {
    ExternalBufferMissing {
        path: AssetPath,
        index: usize,
        reason: String,
    },
    ExternalImageMissing {
        path: AssetPath,
        reason: String,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AssetLoadProgress {
    LoadStarted {
        path: AssetPath,
    },
    CacheHit {
        path: AssetPath,
    },
    AssetFetched {
        path: AssetPath,
        bytes: usize,
    },
    ExternalBufferFetched {
        path: AssetPath,
        index: usize,
        bytes: usize,
    },
    Parsed {
        path: AssetPath,
        nodes: usize,
        meshes: usize,
    },
    Cached {
        path: AssetPath,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub(super) struct AssetLoadTelemetry {
    pub(super) fetched_bytes: usize,
    pub(super) external_buffers: usize,
    pub(super) external_images: usize,
    pub(super) warnings: Vec<AssetLoadWarning>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AssetLoadReportV1 {
    pub schema: String,
    pub path: String,
    pub cache_hit: bool,
    pub fetched_bytes: usize,
    pub external_buffers: usize,
    pub external_images: usize,
    pub provenance: AssetProvenance,
    pub geometry: SceneAssetGeometrySummary,
    pub warnings: Vec<AssetLoadWarningV1>,
    pub progress_events: Vec<AssetLoadProgressV1>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AssetLoadWarningV1 {
    ExternalBufferMissing {
        path: String,
        index: usize,
        reason: String,
    },
    ExternalImageMissing {
        path: String,
        reason: String,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AssetLoadProgressV1 {
    LoadStarted {
        path: String,
    },
    CacheHit {
        path: String,
    },
    AssetFetched {
        path: String,
        bytes: usize,
    },
    ExternalBufferFetched {
        path: String,
        index: usize,
        bytes: usize,
    },
    Parsed {
        path: String,
        nodes: usize,
        meshes: usize,
    },
    Cached {
        path: String,
    },
}

impl Default for AssetLoadControl {
    fn default() -> Self {
        Self::new()
    }
}

impl AssetLoadControl {
    pub fn new() -> Self {
        Self {
            cancelled: Arc::new(AtomicBool::new(false)),
        }
    }

    pub fn cancelled() -> Self {
        let control = Self::new();
        control.cancel();
        control
    }

    pub fn cancel(&self) {
        self.cancelled.store(true, Ordering::SeqCst);
    }

    pub fn is_cancelled(&self) -> bool {
        self.cancelled.load(Ordering::SeqCst)
    }
}

impl<T> AssetLoadReport<T> {
    pub fn asset(&self) -> &T {
        &self.asset
    }

    pub fn into_asset(self) -> T {
        self.asset
    }

    pub fn path(&self) -> &AssetPath {
        &self.path
    }

    pub const fn cache_hit(&self) -> bool {
        self.cache_hit
    }

    pub const fn fetched_bytes(&self) -> usize {
        self.fetched_bytes
    }

    pub const fn external_buffers(&self) -> usize {
        self.external_buffers
    }

    pub const fn external_images(&self) -> usize {
        self.external_images
    }

    pub fn warnings(&self) -> &[AssetLoadWarning] {
        &self.warnings
    }

    pub fn progress_events(&self) -> &[AssetLoadProgress] {
        &self.progress_events
    }
}

impl AssetLoadOptions {
    pub const fn new() -> Self {
        Self {
            strict_textures: false,
            strict_external_resources: false,
        }
    }

    pub const fn with_strict_textures(mut self, strict_textures: bool) -> Self {
        self.strict_textures = strict_textures;
        self
    }

    pub const fn strict_textures(&self) -> bool {
        self.strict_textures
    }

    pub const fn with_strict_external_resources(mut self, strict_external_resources: bool) -> Self {
        self.strict_external_resources = strict_external_resources;
        self
    }

    pub const fn strict_external_resources(&self) -> bool {
        self.strict_external_resources
    }
}

impl AssetLoadReport<SceneAsset> {
    pub fn to_schema_report(&self) -> AssetLoadReportV1 {
        AssetLoadReportV1 {
            schema: ASSET_LOAD_REPORT_SCHEMA_V1.to_owned(),
            path: self.path.as_str().to_owned(),
            cache_hit: self.cache_hit,
            fetched_bytes: self.fetched_bytes,
            external_buffers: self.external_buffers,
            external_images: self.external_images,
            provenance: self.asset.provenance().clone(),
            geometry: self.asset.geometry_summary(),
            warnings: self.warnings.iter().map(AssetLoadWarningV1::from).collect(),
            progress_events: self
                .progress_events
                .iter()
                .map(AssetLoadProgressV1::from)
                .collect(),
        }
    }

    pub fn to_schema_json(&self) -> serde_json::Value {
        serde_json::to_value(self.to_schema_report())
            .expect("asset load report schema contains only serializable fields")
    }
}

impl From<&AssetLoadWarning> for AssetLoadWarningV1 {
    fn from(warning: &AssetLoadWarning) -> Self {
        match warning {
            AssetLoadWarning::ExternalBufferMissing {
                path,
                index,
                reason,
            } => Self::ExternalBufferMissing {
                path: path.as_str().to_owned(),
                index: *index,
                reason: reason.clone(),
            },
            AssetLoadWarning::ExternalImageMissing { path, reason } => Self::ExternalImageMissing {
                path: path.as_str().to_owned(),
                reason: reason.clone(),
            },
        }
    }
}

impl From<&AssetLoadProgress> for AssetLoadProgressV1 {
    fn from(progress: &AssetLoadProgress) -> Self {
        match progress {
            AssetLoadProgress::LoadStarted { path } => Self::LoadStarted {
                path: path.as_str().to_owned(),
            },
            AssetLoadProgress::CacheHit { path } => Self::CacheHit {
                path: path.as_str().to_owned(),
            },
            AssetLoadProgress::AssetFetched { path, bytes } => Self::AssetFetched {
                path: path.as_str().to_owned(),
                bytes: *bytes,
            },
            AssetLoadProgress::ExternalBufferFetched { path, index, bytes } => {
                Self::ExternalBufferFetched {
                    path: path.as_str().to_owned(),
                    index: *index,
                    bytes: *bytes,
                }
            }
            AssetLoadProgress::Parsed {
                path,
                nodes,
                meshes,
            } => Self::Parsed {
                path: path.as_str().to_owned(),
                nodes: *nodes,
                meshes: *meshes,
            },
            AssetLoadProgress::Cached { path } => Self::Cached {
                path: path.as_str().to_owned(),
            },
        }
    }
}

pub(super) fn check_cancelled(
    path: &AssetPath,
    control: Option<&AssetLoadControl>,
) -> Result<(), AssetError> {
    if control.is_some_and(AssetLoadControl::is_cancelled) {
        return Err(AssetError::Cancelled {
            path: path.as_str().to_string(),
            help: "the load was cancelled before parsed asset data was inserted into the cache",
        });
    }
    Ok(())
}

pub(super) fn emit_progress(
    events: &mut Vec<AssetLoadProgress>,
    observer: &mut Option<&mut dyn FnMut(AssetLoadProgress)>,
    event: AssetLoadProgress,
) {
    if let Some(observer) = observer.as_deref_mut() {
        observer(event.clone());
    }
    events.push(event);
}