scena 1.7.2

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

use crate::assets::DefaultAssetFetcher;
use crate::geometry::{GeometryMorphTarget, GeometrySkin};
use crate::scene::recipe::{
    RecipeBuildPolicy, SceneRecipeBuildResourceV1, SceneRecipeDiagnosticV1, SceneRecipeMorphV1,
    SceneRecipeSkinV1,
};
use crate::scene_host::SceneHostCore;
use crate::{GeometryHandle, Vec3};

use super::super::error_diagnostic;
use super::super::policy::RecipeBuildBudget;

pub(in crate::scene_host::recipe) fn build_authored_morphs(
    policy: &RecipeBuildPolicy,
    host: &SceneHostCore<DefaultAssetFetcher>,
    recipes: &[SceneRecipeMorphV1],
    geometries: &BTreeMap<String, GeometryHandle>,
    build_budget: &mut RecipeBuildBudget,
    manifest: &mut Vec<SceneRecipeBuildResourceV1>,
    diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) -> BTreeMap<String, GeometryHandle> {
    let mut handles = BTreeMap::new();
    for (index, recipe) in recipes.iter().enumerate() {
        let path = format!("$.morphs[{index}]");
        let Some(source_handle) = geometries.get(&recipe.source_geometry).copied() else {
            diagnostics.push(error_diagnostic(
                &path,
                "unknown_geometry_ref",
                format!(
                    "morph '{}' references missing source geometry '{}'",
                    recipe.id, recipe.source_geometry
                ),
                "declare the source geometry before the morph",
            ));
            continue;
        };
        let Some(source_geometry) = host.assets.geometry(source_handle) else {
            diagnostics.push(error_diagnostic(
                &path,
                "geometry_missing",
                format!(
                    "morph '{}' source geometry '{}' could not be resolved",
                    recipe.id, recipe.source_geometry
                ),
                "declare a valid source geometry before the morph",
            ));
            continue;
        };
        let targets = recipe
            .targets
            .iter()
            .map(|target| {
                GeometryMorphTarget::new(
                    target
                        .position_deltas
                        .iter()
                        .map(|delta| Vec3::new(delta[0] as f32, delta[1] as f32, delta[2] as f32))
                        .collect(),
                )
            })
            .collect::<Vec<_>>();
        let geometry = match source_geometry.with_morph_targets(targets) {
            Ok(geometry) => geometry,
            Err(error) => {
                diagnostics.push(error_diagnostic(
                    &path,
                    "morph_create_failed",
                    format!("morph '{}' is invalid: {error:?}", recipe.id),
                    "emit one finite position delta per source vertex for every morph target",
                ));
                continue;
            }
        };
        if !passes_geometry_policy(policy, &geometry, &path, &recipe.id, diagnostics) {
            continue;
        }
        let vertex_count = geometry.vertices().len();
        let index_count = geometry.indices().len();
        if let Some(diagnostic) = build_budget.reserve_geometry(
            policy,
            "$.morphs",
            vertex_count as u64,
            index_count as u64,
        ) {
            diagnostics.push(diagnostic);
            continue;
        }
        let handle = host.assets.create_geometry(geometry);
        handles.insert(recipe.id.clone(), handle);
        manifest.push(SceneRecipeBuildResourceV1 {
            id: recipe.id.clone(),
            kind: "morph".to_owned(),
            vertex_count: Some(vertex_count),
            index_count: Some(index_count),
        });
    }
    handles
}

pub(in crate::scene_host::recipe) fn build_authored_skins(
    policy: &RecipeBuildPolicy,
    host: &SceneHostCore<DefaultAssetFetcher>,
    recipes: &[SceneRecipeSkinV1],
    geometries: &BTreeMap<String, GeometryHandle>,
    build_budget: &mut RecipeBuildBudget,
    manifest: &mut Vec<SceneRecipeBuildResourceV1>,
    diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) -> BTreeMap<String, GeometryHandle> {
    let mut handles = BTreeMap::new();
    for (index, recipe) in recipes.iter().enumerate() {
        let path = format!("$.skins[{index}]");
        let Some(source_handle) = geometries.get(&recipe.source_geometry).copied() else {
            diagnostics.push(error_diagnostic(
                &path,
                "unknown_geometry_ref",
                format!(
                    "skin '{}' references missing source geometry '{}'",
                    recipe.id, recipe.source_geometry
                ),
                "declare the source geometry before the skin",
            ));
            continue;
        };
        let Some(source_geometry) = host.assets.geometry(source_handle) else {
            diagnostics.push(error_diagnostic(
                &path,
                "geometry_missing",
                format!(
                    "skin '{}' source geometry '{}' could not be resolved",
                    recipe.id, recipe.source_geometry
                ),
                "declare a valid source geometry before the skin",
            ));
            continue;
        };
        let weights = recipe
            .weights
            .iter()
            .map(|row| [row[0] as f32, row[1] as f32, row[2] as f32, row[3] as f32])
            .collect();
        let geometry = match source_geometry.with_skin(GeometrySkin::new(
            recipe.influence_indices().to_vec(),
            weights,
        )) {
            Ok(geometry) => geometry,
            Err(error) => {
                diagnostics.push(error_diagnostic(
                    &path,
                    "skin_create_failed",
                    format!("skin '{}' is invalid: {error:?}", recipe.id),
                    "emit one four-index and four-weight row per source vertex",
                ));
                continue;
            }
        };
        if !passes_geometry_policy(policy, &geometry, &path, &recipe.id, diagnostics) {
            continue;
        }
        let vertex_count = geometry.vertices().len();
        let index_count = geometry.indices().len();
        if let Some(diagnostic) = build_budget.reserve_geometry(
            policy,
            "$.skins",
            vertex_count as u64,
            index_count as u64,
        ) {
            diagnostics.push(diagnostic);
            continue;
        }
        let handle = host.assets.create_geometry(geometry);
        handles.insert(recipe.id.clone(), handle);
        manifest.push(SceneRecipeBuildResourceV1 {
            id: recipe.id.clone(),
            kind: "skin".to_owned(),
            vertex_count: Some(vertex_count),
            index_count: Some(index_count),
        });
    }
    handles
}

fn passes_geometry_policy(
    policy: &RecipeBuildPolicy,
    geometry: &crate::GeometryDesc,
    path: &str,
    id: &str,
    diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) -> bool {
    let vertex_count = geometry.vertices().len();
    if vertex_count > policy.max_vertices() {
        diagnostics.push(error_diagnostic(
            path,
            "policy_violation",
            format!(
                "geometry '{id}' has {vertex_count} vertices, exceeding RecipeBuildPolicy max_vertices {}",
                policy.max_vertices()
            ),
            "simplify the geometry or raise the operator-owned max_vertices policy",
        ));
        return false;
    }
    let index_count = geometry.indices().len();
    if index_count > policy.max_indices() {
        diagnostics.push(error_diagnostic(
            path,
            "policy_violation",
            format!(
                "geometry '{id}' has {index_count} indices, exceeding RecipeBuildPolicy max_indices {}",
                policy.max_indices()
            ),
            "simplify the geometry or raise the operator-owned max_indices policy",
        ));
        return false;
    }
    true
}