dreamwell-engine 1.0.0

Dreamwell pure-logic engine library — transforms, hierarchy, canon pipeline, spatial math, hashing, tile rules, validation, waymark schema, material/lighting descriptors. No SpacetimeDB dependency.
Documentation
//! Imported scene types — GPU-neutral scene graph from external formats.

use serde::{Deserialize, Serialize};

use super::animation::ImportedAnimation;
use super::mesh::ImportedMesh;
use crate::game_object::Transform;
use crate::material::LitMaterialDesc;

/// A node in the imported scene hierarchy.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneNode {
    /// Node name (from source file).
    pub name: String,
    /// Local transform relative to parent.
    pub transform: Transform,
    /// Index of the mesh in the scene's mesh list, if any.
    pub mesh_index: Option<usize>,
    /// Index of the material in the scene's material list, if any.
    pub material_index: Option<usize>,
    /// Indices of child nodes.
    pub children: Vec<usize>,
}

/// An imported material descriptor.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ImportedMaterial {
    /// Material name (from source file).
    pub name: String,
    /// PBR material properties.
    pub desc: LitMaterialDesc,
}

/// A complete imported scene — nodes, meshes, materials, and animations.
/// Produced by glTF/FBX importers, consumed by the GPU upload pipeline.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ImportedScene {
    /// Scene name.
    pub name: String,
    /// Flat list of scene nodes (index 0 is typically the root).
    pub nodes: Vec<SceneNode>,
    /// Root node indices (scenes may have multiple roots).
    pub roots: Vec<usize>,
    /// Imported meshes referenced by node `mesh_index`.
    pub meshes: Vec<ImportedMesh>,
    /// Imported materials referenced by node `material_index`.
    pub materials: Vec<ImportedMaterial>,
    /// Imported animations.
    pub animations: Vec<ImportedAnimation>,
}

impl ImportedScene {
    /// Total vertex count across all meshes.
    pub fn total_vertices(&self) -> usize {
        self.meshes.iter().map(|m| m.vertex_count()).sum()
    }

    /// Total triangle count across all meshes.
    pub fn total_triangles(&self) -> usize {
        self.meshes.iter().map(|m| m.triangle_count()).sum()
    }

    /// Validate scene integrity.
    pub fn validate(&self) -> Result<(), String> {
        let mesh_count = self.meshes.len();
        let material_count = self.materials.len();
        let node_count = self.nodes.len();

        for (i, node) in self.nodes.iter().enumerate() {
            if let Some(mi) = node.mesh_index {
                if mi >= mesh_count {
                    return Err(format!(
                        "content_scene_mesh_oob:node[{i}] references mesh {mi}, only {mesh_count} meshes"
                    ));
                }
            }
            if let Some(mi) = node.material_index {
                if mi >= material_count {
                    return Err(format!(
                        "content_scene_material_oob:node[{i}] references material {mi}, only {material_count} materials"
                    ));
                }
            }
            for &child in &node.children {
                if child >= node_count {
                    return Err(format!(
                        "content_scene_child_oob:node[{i}] references child {child}, only {node_count} nodes"
                    ));
                }
            }
        }

        for (i, mesh) in self.meshes.iter().enumerate() {
            mesh.validate().map_err(|e| format!("mesh[{i}] '{}': {e}", mesh.name))?;
        }

        for anim in &self.animations {
            anim.validate()?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::content::mesh::ImportedMesh;

    fn simple_scene() -> ImportedScene {
        let mesh = ImportedMesh {
            name: "Cube".into(),
            positions: vec![[0.0; 3]; 3],
            normals: vec![[0.0, 0.0, 1.0]; 3],
            uvs: vec![[0.0; 2]; 3],
            indices: vec![0, 1, 2],
        };
        let mat = ImportedMaterial {
            name: "Default".into(),
            desc: LitMaterialDesc::default(),
        };
        let node = SceneNode {
            name: "Root".into(),
            transform: Transform::default(),
            mesh_index: Some(0),
            material_index: Some(0),
            children: vec![],
        };
        ImportedScene {
            name: "TestScene".into(),
            nodes: vec![node],
            roots: vec![0],
            meshes: vec![mesh],
            materials: vec![mat],
            animations: vec![],
        }
    }

    #[test]
    fn valid_scene() {
        let scene = simple_scene();
        assert!(scene.validate().is_ok());
        assert_eq!(scene.total_vertices(), 3);
        assert_eq!(scene.total_triangles(), 1);
    }

    #[test]
    fn mesh_oob() {
        let mut scene = simple_scene();
        scene.nodes[0].mesh_index = Some(99);
        assert!(scene.validate().unwrap_err().contains("mesh_oob"));
    }

    #[test]
    fn material_oob() {
        let mut scene = simple_scene();
        scene.nodes[0].material_index = Some(99);
        assert!(scene.validate().unwrap_err().contains("material_oob"));
    }

    #[test]
    fn child_oob() {
        let mut scene = simple_scene();
        scene.nodes[0].children.push(99);
        assert!(scene.validate().unwrap_err().contains("child_oob"));
    }

    #[test]
    fn empty_scene() {
        let scene = ImportedScene::default();
        assert!(scene.validate().is_ok());
        assert_eq!(scene.total_vertices(), 0);
    }

    #[test]
    fn imported_material_default() {
        let mat = ImportedMaterial::default();
        assert!(mat.name.is_empty());
        assert_eq!(mat.desc.roughness, 0.5);
    }
}