scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use crate::geometry::Aabb;
use crate::scene::{Camera, CameraKey, Scene, SceneDirtyState, Vec3};

use super::{
    CaptureAutoFrame, CaptureAutoFrameViewport, CaptureCamera, CaptureError, CaptureOptions,
    CapturePoint2, CaptureProjection, CaptureRevisions, CaptureScreenRect, CaptureViewport,
};

pub fn auto_frame_metadata(
    scene: &Scene,
    camera: CameraKey,
    bounds: Aabb,
    viewport_width: u32,
    viewport_height: u32,
) -> Result<CaptureAutoFrame, CaptureError> {
    let mut points = Vec::with_capacity(8);
    for corner in aabb_corners(bounds) {
        let point = scene
            .project_world_point(camera, corner, viewport_width, viewport_height)
            .map_err(CaptureError::from)?
            .ok_or_else(|| CaptureError::AutoFrameProjection {
                reason: "auto-frame projected a bounds corner outside the active camera".to_owned(),
            })?;
        points.push(point);
    }

    let rect = crate::ScreenRect::from_points(&points).ok_or_else(|| {
        CaptureError::AutoFrameProjection {
            reason: "auto-frame projection produced no points".to_owned(),
        }
    })?;
    let projected_rect = CaptureScreenRect::from_rect(rect);
    let viewport_width_f = viewport_width as f32;
    let viewport_height_f = viewport_height as f32;
    let center_error_x = (projected_rect.center_x - viewport_width_f * 0.5).abs();
    let center_error_y = (projected_rect.center_y - viewport_height_f * 0.5).abs();
    let fill_fraction = rect.fill_fraction(viewport_width, viewport_height);
    let inside_viewport = projected_rect.min_x >= -0.5
        && projected_rect.min_y >= -0.5
        && projected_rect.max_x <= viewport_width_f + 0.5
        && projected_rect.max_y <= viewport_height_f + 0.5;
    let centered =
        center_error_x <= viewport_width_f * 0.05 && center_error_y <= viewport_height_f * 0.05;
    let passed = inside_viewport && centered && fill_fraction > 0.2 && fill_fraction <= 0.75;

    Ok(CaptureAutoFrame {
        status: if passed { "passed" } else { "failed" }.to_owned(),
        proof_class: "viewer-level-auto-framing".to_owned(),
        viewport: CaptureAutoFrameViewport {
            width: viewport_width,
            height: viewport_height,
        },
        projected_rect,
        center_error_px: CapturePoint2 {
            x: center_error_x,
            y: center_error_y,
        },
        fill_fraction,
        inside_viewport,
        centered,
    })
}

pub(super) fn capture_auto_frame(
    scene: &Scene,
    camera: CameraKey,
    bounds: Option<Aabb>,
    viewport_width: u32,
    viewport_height: u32,
) -> Result<Option<CaptureAutoFrame>, CaptureError> {
    let Some(bounds) = bounds else {
        return Ok(None);
    };
    if scene.camera(camera).is_none() {
        return Err(CaptureError::NoActiveCameraForAutoFrame);
    }
    Ok(Some(auto_frame_metadata(
        scene,
        camera,
        bounds,
        viewport_width,
        viewport_height,
    )?))
}

pub(super) fn capture_camera(scene: &Scene, camera: CameraKey) -> CaptureCamera {
    let world_transform = scene
        .camera_node(camera)
        .and_then(|node| scene.world_transform(node));
    let projection = scene.camera(camera).map(projection_from_camera);
    CaptureCamera {
        active: projection.is_some(),
        world_transform,
        projection,
    }
}

pub(super) fn revisions_from_dirty(dirty: SceneDirtyState) -> CaptureRevisions {
    CaptureRevisions {
        structure: dirty.structure_revision,
        transform: dirty.transform_revision,
        appearance: dirty
            .appearance_revision
            .saturating_add(dirty.visibility_revision),
        interaction: dirty.interaction_revision,
    }
}

pub(super) fn capture_viewport(
    width: u32,
    height: u32,
    options: CaptureOptions,
) -> Result<CaptureViewport, CaptureError> {
    let dpr = options.device_pixel_ratio;
    if !dpr.is_finite() || dpr <= 0.0 {
        return Err(CaptureError::InvalidDevicePixelRatio { value: dpr });
    }
    let (logical_width, logical_height) = options
        .logical_size
        .unwrap_or((width as f32 / dpr, height as f32 / dpr));
    Ok(CaptureViewport {
        width,
        height,
        logical_width,
        logical_height,
        device_pixel_ratio: dpr,
    })
}

fn projection_from_camera(camera: &Camera) -> CaptureProjection {
    match camera {
        Camera::Perspective(camera) => CaptureProjection::Perspective {
            vertical_fov_radians: camera.vertical_fov.radians(),
            aspect: camera.aspect,
            near: camera.near,
            far: camera.far,
        },
        Camera::Orthographic(camera) => CaptureProjection::Orthographic {
            left: camera.left,
            right: camera.right,
            bottom: camera.bottom,
            top: camera.top,
            near: camera.near,
            far: camera.far,
        },
    }
}

fn aabb_corners(bounds: Aabb) -> [Vec3; 8] {
    [
        Vec3::new(bounds.min.x, bounds.min.y, bounds.min.z),
        Vec3::new(bounds.min.x, bounds.min.y, bounds.max.z),
        Vec3::new(bounds.min.x, bounds.max.y, bounds.min.z),
        Vec3::new(bounds.min.x, bounds.max.y, bounds.max.z),
        Vec3::new(bounds.max.x, bounds.min.y, bounds.min.z),
        Vec3::new(bounds.max.x, bounds.min.y, bounds.max.z),
        Vec3::new(bounds.max.x, bounds.max.y, bounds.min.z),
        Vec3::new(bounds.max.x, bounds.max.y, bounds.max.z),
    ]
}

impl CaptureScreenRect {
    fn from_rect(rect: crate::ScreenRect) -> Self {
        Self {
            min_x: rect.min_x,
            min_y: rect.min_y,
            max_x: rect.max_x,
            max_y: rect.max_y,
            width: rect.width(),
            height: rect.height(),
            center_x: rect.center_x(),
            center_y: rect.center_y(),
        }
    }
}