scena 1.7.2

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

use super::{NodeKey, NodeKind, Scene};

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MeshLodLevel {
    max_screen_fraction: f32,
    geometry: GeometryHandle,
}

impl MeshLodLevel {
    pub fn new(max_screen_fraction: f32, geometry: GeometryHandle) -> Self {
        Self {
            max_screen_fraction,
            geometry,
        }
    }

    pub const fn max_screen_fraction(self) -> f32 {
        self.max_screen_fraction
    }

    pub const fn geometry(self) -> GeometryHandle {
        self.geometry
    }
}

impl Scene {
    pub fn set_mesh_lods(
        &mut self,
        node: NodeKey,
        levels: Vec<MeshLodLevel>,
    ) -> Result<(), LookupError> {
        let Some(node_ref) = self.nodes.get(node) else {
            return Err(LookupError::NodeNotFound(node));
        };
        if !matches!(node_ref.kind, NodeKind::Mesh(_)) {
            return Err(LookupError::InvalidFramingOption {
                field: "node",
                reason: "LOD levels can only be attached to mesh nodes",
            });
        }
        for level in &levels {
            if !level.max_screen_fraction.is_finite()
                || level.max_screen_fraction <= 0.0
                || level.max_screen_fraction > 1.0
            {
                return Err(LookupError::InvalidFramingOption {
                    field: "max_screen_fraction",
                    reason: "LOD thresholds must be finite and in the range (0, 1]",
                });
            }
        }
        let mut levels = levels;
        levels.sort_by(|left, right| {
            left.max_screen_fraction
                .total_cmp(&right.max_screen_fraction)
        });
        if levels.is_empty() {
            self.mesh_lods.remove(&node);
        } else if self.mesh_lods.get(&node) != Some(&levels) {
            self.mesh_lods.insert(node, levels);
            self.structure_revision = self.structure_revision.saturating_add(1);
        }
        Ok(())
    }

    pub(crate) fn mesh_lods(&self, node: NodeKey) -> Option<&[MeshLodLevel]> {
        self.mesh_lods.get(&node).map(Vec::as_slice)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Assets, Color, GeometryDesc, MaterialDesc, Transform};

    #[test]
    fn scene_set_mesh_lods_rejects_invalid_thresholds_without_silent_drop() {
        let assets = Assets::new();
        let base_geometry = assets.create_geometry(GeometryDesc::box_xyz(1.0, 1.0, 1.0));
        let lod_geometry = assets.create_geometry(GeometryDesc::box_xyz(0.5, 0.5, 0.5));
        let material = assets.create_material(MaterialDesc::unlit(Color::WHITE));
        let mut scene = Scene::new();
        let mesh = scene
            .mesh(base_geometry, material)
            .transform(Transform::IDENTITY)
            .add()
            .expect("mesh inserts");

        let error = scene
            .set_mesh_lods(mesh, vec![MeshLodLevel::new(1.25, lod_geometry)])
            .expect_err("invalid threshold must fail closed");

        assert!(matches!(
            error,
            LookupError::InvalidFramingOption {
                field: "max_screen_fraction",
                ..
            }
        ));
        assert!(
            scene.mesh_lods(mesh).is_none(),
            "invalid LOD levels must not be silently filtered into an empty no-op"
        );
    }

    #[test]
    fn scene_set_mesh_lods_rejects_non_mesh_nodes() {
        let assets = Assets::new();
        let lod_geometry = assets.create_geometry(GeometryDesc::box_xyz(0.5, 0.5, 0.5));
        let mut scene = Scene::new();
        let empty = scene
            .add_empty(scene.root(), Transform::IDENTITY)
            .expect("empty node inserts");

        let error = scene
            .set_mesh_lods(empty, vec![MeshLodLevel::new(0.5, lod_geometry)])
            .expect_err("non-mesh LOD attachment must fail closed");

        assert!(matches!(
            error,
            LookupError::InvalidFramingOption { field: "node", .. }
        ));
    }
}