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

use serde::{Deserialize, Serialize};

use super::SceneHostCore;
use super::checks::{region_from_screen, round3};
use crate::geometry::GeometryTopology;
use crate::{
    AssetFetcher, CaptureRgba8, CaptureScreenRect, CaptureScreenRegion, SceneCompositionRegionV1,
    screen_region_from_points, screen_region_from_rect, transform_point_for_projection,
};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompositionOverlaySegmentV1 {
    pub handle: Option<u64>,
    pub start_css_px: [f32; 2],
    pub end_css_px: [f32; 2],
    pub region: SceneCompositionRegionV1,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct CompositionLabelRegion {
    pub(super) clipped: CaptureScreenRegion,
    pub(super) unclipped: CaptureScreenRect,
}

impl<F: AssetFetcher> SceneHostCore<F> {
    pub(super) fn composition_label_region_infos(
        &self,
        capture: &CaptureRgba8,
    ) -> BTreeMap<u64, CompositionLabelRegion> {
        let mut regions = BTreeMap::new();
        for (node, _label_key, label, transform) in self.scene.label_nodes() {
            if !self.scene.visible_for_active_camera(node) {
                continue;
            }
            let Some(projected) =
                crate::project_world_point_from_capture(capture, transform.translation)
            else {
                continue;
            };
            let metrics = label.metrics();
            let padding = (label.size() * 0.25).ceil().max(2.0);
            let Some(unclipped) = label_screen_rect_from_center_size(
                projected.x,
                projected.y,
                metrics.width_px,
                metrics.height_px,
                padding,
            ) else {
                continue;
            };
            let Some(clipped) = screen_region_from_rect(
                unclipped,
                capture.descriptor.width,
                capture.descriptor.height,
            ) else {
                continue;
            };
            if let Some(handle) = self.node_handle_map.get(&node).copied() {
                regions.insert(handle, CompositionLabelRegion { clipped, unclipped });
            }
        }
        regions
    }

    pub(super) fn composition_overlay_segments(
        &self,
        capture: &CaptureRgba8,
    ) -> Vec<CompositionOverlaySegmentV1> {
        let mut segments = Vec::new();
        for (node, mesh, transform) in self.scene.mesh_nodes() {
            if !self.scene.visible_for_active_camera(node) {
                continue;
            }
            let Some(geometry) = self.assets.geometry(mesh.geometry()) else {
                continue;
            };
            if geometry.topology() != GeometryTopology::Lines {
                continue;
            }
            let world = self.scene.world_transform(node).unwrap_or(transform);
            let vertices = geometry.vertices();
            let padding = self
                .assets
                .material(mesh.material())
                .and_then(|material| material.stroke_width_px())
                .unwrap_or(1.0)
                .ceil()
                .max(2.0);
            for segment in geometry.indices().chunks_exact(2) {
                let Some(start) = vertices.get(segment[0] as usize) else {
                    continue;
                };
                let Some(end) = vertices.get(segment[1] as usize) else {
                    continue;
                };
                let start = transform_point_for_projection(world, start.position);
                let end = transform_point_for_projection(world, end.position);
                let Some(start) = crate::project_world_point_from_capture(capture, start) else {
                    continue;
                };
                let Some(end) = crate::project_world_point_from_capture(capture, end) else {
                    continue;
                };
                let Some(region) = screen_region_from_points(
                    &[(start.x, start.y), (end.x, end.y)],
                    padding,
                    capture.descriptor.width,
                    capture.descriptor.height,
                ) else {
                    continue;
                };
                segments.push(CompositionOverlaySegmentV1 {
                    handle: self.node_handle_map.get(&node).copied(),
                    start_css_px: [round3(start.x), round3(start.y)],
                    end_css_px: [round3(end.x), round3(end.y)],
                    region: region_from_screen(
                        "line",
                        self.node_handle_map.get(&node).copied(),
                        region,
                    ),
                });
            }
        }
        segments
    }
}

fn label_screen_rect_from_center_size(
    center_x: f32,
    center_y: f32,
    width_px: f32,
    height_px: f32,
    padding_px: f32,
) -> Option<CaptureScreenRect> {
    if !center_x.is_finite()
        || !center_y.is_finite()
        || !width_px.is_finite()
        || !height_px.is_finite()
        || !padding_px.is_finite()
    {
        return None;
    }
    let half_width = (width_px * 0.5).max(0.0);
    let half_height = (height_px * 0.5).max(0.0);
    let min_x = center_x - half_width - padding_px;
    let min_y = center_y - half_height - padding_px;
    let max_x = center_x + half_width + padding_px;
    let max_y = center_y + half_height + padding_px;
    if !min_x.is_finite() || !min_y.is_finite() || !max_x.is_finite() || !max_y.is_finite() {
        return None;
    }
    Some(CaptureScreenRect {
        min_x: round3(min_x),
        min_y: round3(min_y),
        max_x: round3(max_x),
        max_y: round3(max_y),
        width: round3((max_x - min_x).max(0.0)),
        height: round3((max_y - min_y).max(0.0)),
        center_x: round3((min_x + max_x) * 0.5),
        center_y: round3((min_y + max_y) * 0.5),
    })
}