scena 1.3.0

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

use crate::{CameraKey, ConnectorFrame, NodeKey, Scene, Transform, Vec3};

#[derive(Debug, Clone, Copy)]
pub(super) struct ConnectorMarker {
    pub(super) node: NodeKey,
    pub(super) local_transform: Transform,
}

pub(super) fn connector_marker(
    import: &crate::SceneImport,
    name: &str,
) -> Result<ConnectorMarker, JsValue> {
    let connector = import
        .connector(name)
        .map_err(|err| JsValue::from_str(&format!("connector {name:?} missing: {err:?}")))?;
    let frame = ConnectorFrame::from_import_connector(connector);
    Ok(ConnectorMarker {
        node: frame.node(),
        local_transform: frame.local_transform(),
    })
}

pub(super) fn project_connector_marker(
    scene: &Scene,
    camera: CameraKey,
    marker: ConnectorMarker,
    viewport_width: u32,
    viewport_height: u32,
) -> Result<JsValue, JsValue> {
    let world = marker_world_transform(scene, marker)
        .ok_or_else(|| JsValue::from_str("connector marker node transform missing"))?;
    let projected = scene
        .project_world_point(
            camera,
            world.translation,
            viewport_width.max(1),
            viewport_height.max(1),
        )
        .map_err(|err| JsValue::from_str(&format!("project_world_point failed: {err:?}")))?;
    let object = js_sys::Object::new();
    match projected {
        Some(projected) => {
            set_object_bool(&object, "visible", true)?;
            set_object_number(&object, "x", projected.x as f64)?;
            set_object_number(&object, "y", projected.y as f64)?;
            set_object_number(&object, "depth", projected.depth as f64)?;
        }
        None => {
            set_object_bool(&object, "visible", false)?;
        }
    }
    Ok(object.into())
}

fn marker_world_transform(scene: &Scene, marker: ConnectorMarker) -> Option<Transform> {
    let node_world = scene.world_transform(marker.node)?;
    Some(compose_marker_transform(node_world, marker.local_transform))
}

fn compose_marker_transform(parent: Transform, child: Transform) -> Transform {
    let scaled_child_translation = Vec3::new(
        child.translation.x * parent.scale.x,
        child.translation.y * parent.scale.y,
        child.translation.z * parent.scale.z,
    );
    Transform {
        translation: parent.translation + parent.rotation * scaled_child_translation,
        rotation: normalize_rotation(parent.rotation * child.rotation),
        scale: Vec3::new(
            parent.scale.x * child.scale.x,
            parent.scale.y * child.scale.y,
            parent.scale.z * child.scale.z,
        ),
    }
}

fn normalize_rotation(rotation: crate::Quat) -> crate::Quat {
    let length_sq = rotation.length_squared();
    if length_sq <= f32::EPSILON || !length_sq.is_finite() {
        return crate::Quat::IDENTITY;
    }
    rotation.normalize()
}

pub(super) fn set_object_value(
    object: &js_sys::Object,
    key: &str,
    value: JsValue,
) -> Result<(), JsValue> {
    js_sys::Reflect::set(object, &JsValue::from_str(key), &value).map(|_| ())
}

fn set_object_number(object: &js_sys::Object, key: &str, value: f64) -> Result<(), JsValue> {
    set_object_value(object, key, JsValue::from_f64(value))
}

fn set_object_bool(object: &js_sys::Object, key: &str, value: bool) -> Result<(), JsValue> {
    set_object_value(object, key, JsValue::from_bool(value))
}