scena 1.7.1

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

use super::{SceneHostCore, SceneHostError, SceneHostErrorCode};
use crate::assets::{GeometryHandle, MaterialHandle, SceneAsset, SceneAssetMesh};
use crate::{
    AssetFetcher, Color, InstanceId, InstanceSetKey, NodeKey, SceneHostInstanceEntryInspectionV1,
    SceneHostInstanceSetInspectionV1, Transform,
};

pub(super) const INSTANCE_HANDLE_GENERATION_BASE: u32 = 1_000_000;

#[derive(Debug, Clone, PartialEq)]
pub(super) struct HostInstanceBinding {
    pub(super) entries: Vec<HostInstanceEntry>,
    pub(super) root_transform: Transform,
    pub(super) visible: bool,
    pub(super) tint: Option<Color>,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct HostInstanceEntry {
    pub(super) set_node: NodeKey,
    pub(super) set: InstanceSetKey,
    pub(super) instance: InstanceId,
    pub(super) local_transform: Transform,
}

#[derive(Debug, Clone, PartialEq)]
struct InstancedDrawable {
    geometry: GeometryHandle,
    material: MaterialHandle,
    local_transform: Transform,
}

#[derive(Debug, Clone)]
struct InstanceSetBuild {
    set_node: NodeKey,
    set: InstanceSetKey,
    geometry: GeometryHandle,
    material: MaterialHandle,
    local_transforms: Vec<Transform>,
}

impl<F: AssetFetcher> SceneHostCore<F> {
    pub(super) fn instantiate_scene_asset_instanced_under(
        &mut self,
        parent: NodeKey,
        scene_asset: &SceneAsset,
        count: usize,
    ) -> Result<Vec<u64>, SceneHostError> {
        if count == 0 {
            return Err(invalid_input(
                "instanced scene import count must be greater than zero",
            ));
        }

        let drawables = collect_drawables(scene_asset);
        if drawables.is_empty() {
            return Err(invalid_input(
                "instanced scene import requires at least one mesh primitive",
            ));
        }

        let mut builds: Vec<InstanceSetBuild> = Vec::new();
        for drawable in drawables {
            if let Some(build) = builds.iter_mut().find(|build| {
                build.geometry == drawable.geometry && build.material == drawable.material
            }) {
                build.local_transforms.push(drawable.local_transform);
                continue;
            }
            let (set_node, set) = self.scene.add_instance_set_node(
                parent,
                drawable.geometry,
                drawable.material,
                Transform::IDENTITY,
            )?;
            self.register_node(set_node);
            builds.push(InstanceSetBuild {
                set_node,
                set,
                geometry: drawable.geometry,
                material: drawable.material,
                local_transforms: vec![drawable.local_transform],
            });
        }

        for build in &builds {
            self.scene.reserve_instances(
                build.set,
                count.saturating_mul(build.local_transforms.len()),
            )?;
        }

        let mut handles = Vec::with_capacity(count);
        for _ in 0..count {
            let root_transform = Transform::IDENTITY;
            let mut entries = Vec::new();
            for build in &builds {
                for local_transform in &build.local_transforms {
                    let instance = self.scene.push_instance(
                        build.set,
                        Transform::compose(root_transform, *local_transform),
                    )?;
                    entries.push(HostInstanceEntry {
                        set_node: build.set_node,
                        set: build.set,
                        instance,
                        local_transform: *local_transform,
                    });
                }
            }
            let binding = HostInstanceBinding {
                entries,
                root_transform,
                visible: true,
                tint: None,
            };
            let handle = self.instance_handles.insert(binding.clone());
            for entry in binding.entries {
                self.instance_handle_map
                    .insert((entry.set_node, entry.instance), handle);
            }
            handles.push(handle);
        }

        Ok(handles)
    }

    pub(super) fn set_instance_root_transform(
        &mut self,
        handle: u64,
        transform: Transform,
    ) -> Result<(), SceneHostError> {
        let binding = self.instance_binding(handle)?.clone();
        if binding.root_transform == transform {
            return Ok(());
        }
        for entry in &binding.entries {
            self.scene.set_instance_transform(
                entry.set,
                entry.instance,
                Transform::compose(transform, entry.local_transform),
            )?;
        }
        self.instance_binding_mut(handle)?.root_transform = transform;
        Ok(())
    }

    pub(super) fn set_instance_root_visible(
        &mut self,
        handle: u64,
        visible: bool,
    ) -> Result<(), SceneHostError> {
        let binding = self.instance_binding(handle)?.clone();
        if binding.visible == visible {
            return Ok(());
        }
        for entry in &binding.entries {
            self.scene
                .set_instance_visible(entry.set, entry.instance, visible)?;
        }
        self.instance_binding_mut(handle)?.visible = visible;
        Ok(())
    }

    pub(super) fn set_instance_root_tint(
        &mut self,
        handle: u64,
        tint: Option<Color>,
    ) -> Result<(), SceneHostError> {
        if tint.is_some_and(|tint| tint.a < 1.0) {
            return Err(invalid_input(
                "instanced scene roots only accept opaque per-instance tint in this release",
            ));
        }
        let binding = self.instance_binding(handle)?.clone();
        if binding.tint == tint {
            return Ok(());
        }
        for entry in &binding.entries {
            self.scene
                .set_instance_tint(entry.set, entry.instance, tint)?;
        }
        self.instance_binding_mut(handle)?.tint = tint;
        Ok(())
    }

    pub(super) fn remove_instance_root(&mut self, handle: u64) -> Result<(), SceneHostError> {
        let binding = self.instance_handles.remove(
            handle,
            SceneHostErrorCode::NodeHandleNotFound,
            SceneHostErrorCode::StaleNodeHandle,
        )?;
        for entry in binding.entries {
            self.scene.remove_instance(entry.set, entry.instance)?;
            self.instance_handle_map
                .remove(&(entry.set_node, entry.instance));
        }
        Ok(())
    }

    pub(super) fn invalidate_instance_bindings_for_nodes(&mut self, removed_nodes: &[NodeKey]) {
        if removed_nodes.is_empty() {
            return;
        }
        let removed_nodes = removed_nodes.iter().copied().collect::<BTreeSet<_>>();
        let handles = self
            .instance_handle_map
            .iter()
            .filter_map(|((node, _instance), handle)| {
                removed_nodes.contains(node).then_some(*handle)
            })
            .collect::<BTreeSet<_>>();
        for handle in handles {
            let Ok(binding) = self.instance_handles.remove(
                handle,
                SceneHostErrorCode::NodeHandleNotFound,
                SceneHostErrorCode::StaleNodeHandle,
            ) else {
                continue;
            };
            for entry in binding.entries {
                self.instance_handle_map
                    .remove(&(entry.set_node, entry.instance));
            }
        }
    }

    pub(super) fn instance_bindings_report(&self) -> Vec<SceneHostInstanceSetInspectionV1> {
        let handles = self
            .instance_handle_map
            .values()
            .copied()
            .collect::<BTreeSet<_>>();
        handles
            .into_iter()
            .filter_map(|handle| {
                let binding = self.instance_binding(handle).ok()?;
                let entries = binding
                    .entries
                    .iter()
                    .map(|entry| SceneHostInstanceEntryInspectionV1 {
                        set_node: self.node_handle_map.get(&entry.set_node).copied(),
                        instance_id: entry.instance.as_u64(),
                        local_transform: entry.local_transform,
                    })
                    .collect::<Vec<_>>();
                Some(SceneHostInstanceSetInspectionV1 {
                    root_handle: handle,
                    visible: binding.visible,
                    tint: binding.tint,
                    root_transform: binding.root_transform,
                    entries,
                })
            })
            .collect()
    }

    fn instance_binding(&self, handle: u64) -> Result<&HostInstanceBinding, SceneHostError> {
        self.instance_handles.get(
            handle,
            SceneHostErrorCode::NodeHandleNotFound,
            SceneHostErrorCode::StaleNodeHandle,
        )
    }

    fn instance_binding_mut(
        &mut self,
        handle: u64,
    ) -> Result<&mut HostInstanceBinding, SceneHostError> {
        self.instance_handles.get_mut(
            handle,
            SceneHostErrorCode::NodeHandleNotFound,
            SceneHostErrorCode::StaleNodeHandle,
        )
    }
}

fn collect_drawables(scene_asset: &SceneAsset) -> Vec<InstancedDrawable> {
    let mut child_indices = BTreeSet::new();
    for node in scene_asset.nodes() {
        child_indices.extend(node.children().iter().copied());
    }

    let mut drawables = Vec::new();
    for root in (0..scene_asset.nodes().len()).filter(|index| !child_indices.contains(index)) {
        collect_node_drawables(scene_asset, root, Transform::IDENTITY, &mut drawables);
    }
    drawables
}

fn collect_node_drawables(
    scene_asset: &SceneAsset,
    node_index: usize,
    parent_from_asset: Transform,
    drawables: &mut Vec<InstancedDrawable>,
) {
    let Some(node) = scene_asset.nodes().get(node_index) else {
        return;
    };
    let node_from_asset = Transform::compose(parent_from_asset, node.transform());
    for mesh in node.meshes() {
        collect_mesh_drawables(mesh, node_from_asset, node.instance_transforms(), drawables);
    }
    for child in node.children() {
        collect_node_drawables(scene_asset, *child, node_from_asset, drawables);
    }
}

fn collect_mesh_drawables(
    mesh: &SceneAssetMesh,
    node_from_asset: Transform,
    instance_transforms: &[Transform],
    drawables: &mut Vec<InstancedDrawable>,
) {
    if instance_transforms.is_empty() {
        drawables.push(InstancedDrawable {
            geometry: mesh.geometry(),
            material: mesh.material(),
            local_transform: node_from_asset,
        });
        return;
    }
    for instance_transform in instance_transforms {
        drawables.push(InstancedDrawable {
            geometry: mesh.geometry(),
            material: mesh.material(),
            local_transform: Transform::compose(node_from_asset, *instance_transform),
        });
    }
}

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