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::BTreeSet;

use serde_json::Value;

use crate::scene::recipe::types::SceneRecipeDiagnosticV1;
use crate::scene::recipe::validation::diagnostic;

use super::super::finite_vec3;

pub(in crate::scene::recipe::validation::authoring) fn validate_look_at_target(
    path: &str,
    value: Option<&Value>,
    node_ids: &BTreeSet<String>,
    import_ids: &BTreeSet<String>,
    diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) {
    match value {
        Some(Value::String(target))
            if is_declared_node_or_import_path(target, node_ids, import_ids) => {}
        Some(Value::String(target)) if is_import_path_ref(target) => {
            diagnostics.push(unknown_import_path_diagnostic(path, target));
        }
        Some(Value::String(target)) => diagnostics.push(diagnostic(
            "unknown_node_ref",
            "error",
            path,
            format!("look_at target references unknown node '{target}'"),
            "target an authored node id, imported node path, or provide a [x,y,z] position",
            None,
            false,
        )),
        Some(value) if finite_vec3(value).is_some() => {}
        Some(_) => diagnostics.push(diagnostic(
            "invalid_look_at_target",
            "error",
            path,
            "look_at target must be an authored node id, imported node path, or finite [x,y,z] position",
            "target an authored node id, imported node path such as part:/Node, or provide a [x,y,z] position",
            None,
            false,
        )),
        None => diagnostics.push(diagnostic(
            "missing_look_at_target",
            "error",
            path,
            "look_at transform requires a target",
            "target an authored node id, imported node path, or provide a [x,y,z] position",
            None,
            false,
        )),
    }
}

pub(in crate::scene::recipe::validation::authoring) fn validate_node_ref(
    path: &str,
    value: Option<&Value>,
    node_ids: &BTreeSet<String>,
    import_ids: &BTreeSet<String>,
    diagnostics: &mut Vec<SceneRecipeDiagnosticV1>,
) {
    match value.and_then(Value::as_str) {
        Some(value) if is_declared_node_or_import_path(value, node_ids, import_ids) => {}
        Some(value) if is_import_path_ref(value) => {
            diagnostics.push(unknown_import_path_diagnostic(path, value));
        }
        Some(value) => diagnostics.push(diagnostic(
            "unknown_node_ref",
            "error",
            path,
            format!("node reference '{value}' does not name a declared authored node or imported node path"),
            "declare a node with this id or target an imported node path such as part:/Node",
            None,
            false,
        )),
        None => diagnostics.push(diagnostic(
            "missing_node_ref",
            "error",
            path,
            "node reference must be a string",
            "set node to an authored node id or imported node path such as part:/Node",
            None,
            false,
        )),
    }
}

fn is_declared_node_or_import_path(
    value: &str,
    node_ids: &BTreeSet<String>,
    import_ids: &BTreeSet<String>,
) -> bool {
    node_ids.contains(value)
        || import_path_import(value).is_some_and(|import| import_ids.contains(import))
}

fn is_import_path_ref(value: &str) -> bool {
    import_path_import(value).is_some()
}

fn import_path_import(value: &str) -> Option<&str> {
    let (import, _path) = value.split_once(":/")?;
    (!import.trim().is_empty()).then_some(import)
}

fn unknown_import_path_diagnostic(path: &str, value: &str) -> SceneRecipeDiagnosticV1 {
    let import = import_path_import(value).unwrap_or(value);
    diagnostic(
        "unknown_import_ref",
        "error",
        path,
        format!("imported node path '{value}' references unknown import '{import}'"),
        "declare the import id before targeting imported node paths",
        None,
        false,
    )
}