scena 1.7.0

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

use super::{SceneHostCore, SceneHostError, SceneHostErrorCode};
use crate::{AntiAliasing, AssetFetcher, PostBloomConfig, ScreenSpaceAmbientOcclusionConfig};

impl<F: AssetFetcher> SceneHostCore<F> {
    pub fn set_anti_aliasing(&mut self, mode: &str) -> Result<(), SceneHostError> {
        let anti_aliasing = match mode {
            "fxaa" | "on" | "true" => AntiAliasing::Fxaa,
            "none" | "off" | "false" => AntiAliasing::None,
            other => {
                return Err(invalid_input(format!(
                    "unsupported SceneHost anti-aliasing mode {other}"
                )));
            }
        };
        self.renderer.set_anti_aliasing(anti_aliasing);
        Ok(())
    }

    pub fn set_bloom_json(&mut self, config_json: Option<&str>) -> Result<(), SceneHostError> {
        let config = config_json.map(parse_bloom_config).transpose()?;
        self.renderer.set_bloom(config);
        Ok(())
    }

    pub fn set_ambient_occlusion_json(
        &mut self,
        config_json: Option<&str>,
    ) -> Result<(), SceneHostError> {
        let config = config_json
            .map(parse_ambient_occlusion_config)
            .transpose()?;
        self.renderer.set_screen_space_ambient_occlusion(config);
        Ok(())
    }
}

fn parse_bloom_config(json: &str) -> Result<PostBloomConfig, SceneHostError> {
    let value: Value = serde_json::from_str(json)
        .map_err(|error| invalid_input(format!("invalid bloom config JSON: {error}")))?;
    let default = PostBloomConfig::subtle();
    Ok(PostBloomConfig::new(
        u8_field(&value, "threshold_srgb", default.threshold_srgb())?,
        f32_field(&value, "intensity", default.intensity())?,
        u8_field(&value, "radius_px", default.radius_px())?,
    ))
}

fn parse_ambient_occlusion_config(
    json: &str,
) -> Result<ScreenSpaceAmbientOcclusionConfig, SceneHostError> {
    let value: Value = serde_json::from_str(json).map_err(|error| {
        invalid_input(format!("invalid ambient occlusion config JSON: {error}"))
    })?;
    let default = ScreenSpaceAmbientOcclusionConfig::subtle();
    Ok(ScreenSpaceAmbientOcclusionConfig::new(
        u8_field(&value, "radius_px", default.radius_px())?,
        f32_field(&value, "intensity", default.intensity())?,
        f32_field(&value, "depth_threshold", default.depth_threshold())?,
    ))
}

fn u8_field(value: &Value, field: &str, default: u8) -> Result<u8, SceneHostError> {
    let Some(value) = value.get(field) else {
        return Ok(default);
    };
    let Some(value) = value.as_u64() else {
        return Err(invalid_input(format!(
            "{field} must be an unsigned integer"
        )));
    };
    Ok(value.min(u64::from(u8::MAX)) as u8)
}

fn f32_field(value: &Value, field: &str, default: f32) -> Result<f32, SceneHostError> {
    let Some(value) = value.get(field) else {
        return Ok(default);
    };
    let Some(value) = value.as_f64() else {
        return Err(invalid_input(format!("{field} must be a number")));
    };
    Ok(value as f32)
}

fn invalid_input(message: impl Into<String>) -> SceneHostError {
    SceneHostError::new(SceneHostErrorCode::InvalidInput, message.into())
}