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::scene::recipe::{
    SceneRecipeColorV1, SceneRecipeDiagnosticV1, SceneRecipeGridReflectionV1, SceneRecipeGridV1,
};
use crate::scene_host::SceneHostCore;
use crate::{Color, GeometryDesc, GridFloorOptions, MaterialDesc, Transform, Vec3};

use super::super::authoring::{DiagnosticPathExt, authored_color};
use super::super::scene_host_error_diagnostic;

pub(super) fn apply_grid(
    host: &mut SceneHostCore<DefaultAssetFetcher>,
    colors: &BTreeMap<String, SceneRecipeColorV1>,
    grid: &SceneRecipeGridV1,
    diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) {
    let mut options = GridFloorOptions::new();
    if grid.under_bounds {
        options = grid_options_under_scene_bounds(host, options);
    }
    if let Some(floor_y) = grid.floor_y {
        options = options.floor_y(floor_y as f32);
    }
    if let Some(padding) = grid.padding {
        options = options.padding(padding as f32);
    }
    if let Some(line_spacing) = grid.line_spacing {
        options = options.line_spacing(line_spacing as f32);
    }
    if let Some(line_width_px) = grid.line_width_px {
        options = options.line_width_px(line_width_px as f32);
    }
    if let Some(color) = grid.color.as_deref() {
        match authored_color(colors, color) {
            Ok(color) => options = options.color(color),
            Err(diagnostic) => {
                diagnostics.push((*diagnostic).with_path("$.scene.grid.color".to_owned()));
                return;
            }
        }
    }
    if let Some(color) = grid.line_color.as_deref() {
        match authored_color(colors, color) {
            Ok(color) => options = options.line_color(color),
            Err(diagnostic) => {
                diagnostics.push((*diagnostic).with_path("$.scene.grid.line_color".to_owned()));
                return;
            }
        }
    }
    if let Some(roughness) = grid.roughness {
        options = options.roughness(roughness as f32);
    }
    let reflection_sources = grid
        .reflection
        .filter(|reflection| reflection.enabled)
        .map(|reflection| collect_reflection_sources(host, options.floor_y_value(), reflection))
        .unwrap_or_default();
    add_grid_floor_with_sources(
        host,
        options,
        grid.reflection,
        &reflection_sources,
        diagnostics,
    );
}

pub(super) fn grid_options_under_scene_bounds(
    host: &SceneHostCore<DefaultAssetFetcher>,
    mut options: GridFloorOptions,
) -> GridFloorOptions {
    if let Ok(Some(bounds)) = host
        .scene
        .node_world_bounds(host.scene.root(), &host.assets)
    {
        options = options.under_bounds(bounds);
    }
    options
}

pub(super) fn add_grid_floor_with_options(
    host: &mut SceneHostCore<DefaultAssetFetcher>,
    options: GridFloorOptions,
    reflection: Option<SceneRecipeGridReflectionV1>,
    diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) {
    let reflection_sources = reflection
        .filter(|reflection| reflection.enabled)
        .map(|reflection| collect_reflection_sources(host, options.floor_y_value(), reflection))
        .unwrap_or_default();
    add_grid_floor_with_sources(host, options, reflection, &reflection_sources, diagnostics);
}

fn add_grid_floor_with_sources(
    host: &mut SceneHostCore<DefaultAssetFetcher>,
    options: GridFloorOptions,
    reflection: Option<SceneRecipeGridReflectionV1>,
    reflection_sources: &[GridReflectionSource],
    diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) {
    match host.scene.add_grid_floor(&host.assets, options) {
        Ok(handles) => {
            host.register_node(handles.slab);
            host.register_node(handles.grid);
            add_grid_reflection_decals(
                host,
                handles.bounds.min.y,
                reflection_sources,
                reflection,
                diagnostics,
            );
        }
        Err(error) => diagnostics.push(scene_host_error_diagnostic(
            "$.scene.grid",
            "grid_create_failed",
            error.into(),
        )),
    }
}

#[derive(Debug, Clone, Copy)]
struct GridReflectionSource {
    center: Vec3,
    width: f32,
    depth: f32,
    height: f32,
    color: Color,
}

fn collect_reflection_sources(
    host: &SceneHostCore<DefaultAssetFetcher>,
    floor_y: f32,
    reflection: SceneRecipeGridReflectionV1,
) -> Vec<GridReflectionSource> {
    let strength = reflection.strength.unwrap_or(0.42) as f32;
    host.scene
        .mesh_nodes()
        .filter_map(|(node, mesh, _transform)| {
            let bounds = host
                .scene
                .node_world_bounds(node, &host.assets)
                .ok()
                .flatten()?;
            let extent = bounds.max - bounds.min;
            if extent.y <= 0.02 || bounds.max.y <= floor_y + 0.01 {
                return None;
            }
            let material = host.assets.material(mesh.material())?;
            let color = reflection_color(material.base_color(), strength);
            Some(GridReflectionSource {
                center: bounds.center(),
                width: extent.x.max(0.08),
                depth: (extent.z + extent.y * 0.70).max(0.10),
                height: extent.y,
                color,
            })
        })
        .collect()
}

fn add_grid_reflection_decals(
    host: &mut SceneHostCore<DefaultAssetFetcher>,
    floor_y: f32,
    sources: &[GridReflectionSource],
    reflection: Option<SceneRecipeGridReflectionV1>,
    diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) {
    let Some(reflection) = reflection.filter(|reflection| reflection.enabled) else {
        return;
    };
    let strength = reflection.strength.unwrap_or(0.42) as f32;
    for (index, source) in sources.iter().enumerate() {
        let geometry = host.assets.create_geometry(GeometryDesc::plane(
            source.width * (1.0 + strength * 0.20),
            source.depth,
        ));
        let material = host
            .assets
            .create_material(MaterialDesc::unlit(source.color).with_double_sided(true));
        let z_offset = (source.height * 0.10).min(source.depth * 0.25);
        let transform = Transform::at(Vec3::new(
            source.center.x,
            floor_y + 0.003 + index as f32 * 0.0005,
            source.center.z + z_offset,
        ));
        match host
            .scene
            .mesh(geometry, material)
            .transform(transform)
            .add()
        {
            Ok(node) => {
                host.register_node(node);
            }
            Err(error) => diagnostics.push(scene_host_error_diagnostic(
                "$.scene.grid.reflection",
                "grid_reflection_create_failed",
                error.into(),
            )),
        }
    }
}

fn reflection_color(color: Color, strength: f32) -> Color {
    let strength = strength.clamp(0.0, 1.0);
    Color::from_linear_rgba(
        (color.r * strength).clamp(0.0, 1.0),
        (color.g * strength).clamp(0.0, 1.0),
        (color.b * strength).clamp(0.0, 1.0),
        1.0,
    )
}