scena 1.7.2

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

use super::checks::{CompositionCheckExt, checked_check, error_check, observed_pairs};
use super::helpers::{
    clipped_region_from_rect, draws_for_handle, linear_rgba_to_srgb8, projected_node_rect,
    resolve_target_handles,
};
use crate::{
    CaptureRgba8, CaptureScreenRegion, SceneCompositionCheckV1, SceneInspectionReportV1,
    SceneRecipeBuildV1, SceneRecipeExpectV1,
};

const DEFAULT_HELPER_COLOR_TOLERANCE: u8 = 56;
const OCCLUDER_INTERIOR_INSET_PX: u32 = 6;

pub(super) fn composition_helper_layer_checks(
    expect: Option<&SceneRecipeExpectV1>,
    manifest: &SceneRecipeBuildV1,
    inspection: &SceneInspectionReportV1,
    capture: &CaptureRgba8,
) -> Vec<SceneCompositionCheckV1> {
    let mut checks = Vec::new();
    let Some(expect) = expect else {
        return checks;
    };
    for expectation in &expect.expect_helper_occluded {
        let id = format!("expect_helper_occluded.{}", expectation.id);
        let target_id = Some(expectation.id.clone());
        let helper_handles = resolve_target_handles(&expectation.helper, manifest);
        let occluder_handles = resolve_target_handles(&expectation.occluder, manifest);
        let affected = combined_handles(&helper_handles, &occluder_handles);
        if helper_handles.is_empty() || occluder_handles.is_empty() {
            checks.push(error_check(
                id,
                "helper_render_layer",
                "helper_occlusion_target_unresolved",
                target_id,
                affected,
                observed_pairs([
                    ("helper_handles", json!(helper_handles)),
                    ("occluder_handles", json!(occluder_handles)),
                ]),
                (
                    "helper occlusion expectation did not resolve both helper and occluder targets",
                    "fix the helper/occluder target ids or remove the stale helper occlusion expectation",
                ),
            ));
            continue;
        }

        let Some(region) = occluder_interior_region(capture, inspection, &occluder_handles) else {
            checks.push(error_check(
                id,
                "helper_render_layer",
                "helper_occlusion_region_unavailable",
                target_id,
                affected,
                observed_pairs([
                    ("helper_handles", json!(helper_handles)),
                    ("occluder_handles", json!(occluder_handles)),
                ]),
                (
                    "helper occlusion expectation could not derive a stable occluder screen region",
                    "keep the occluder visible and large enough in frame or remove the expectation",
                ),
            ));
            continue;
        };

        let Some(helper_color) = helper_srgb8_color(inspection, &helper_handles) else {
            checks.push(error_check(
                id,
                "helper_render_layer",
                "helper_occlusion_color_unavailable",
                target_id,
                affected,
                observed_pairs([("helper_handles", json!(helper_handles))]),
                (
                    "helper occlusion expectation could not determine the helper draw color",
                    "use an authored helper material with a stable base color",
                ),
            ));
            continue;
        };

        let helper_pixels = count_helper_pixels_in_region(capture, region, helper_color);
        let tolerance = u64::from(expectation.tolerance_pixels.unwrap_or(0));
        let observed = observed_pairs([
            ("helper_handles", json!(helper_handles)),
            ("occluder_handles", json!(occluder_handles)),
            (
                "helper_srgb8",
                json!([helper_color[0], helper_color[1], helper_color[2]]),
            ),
            ("helper_pixels_inside_occluder", json!(helper_pixels)),
            ("tolerance_pixels", json!(tolerance)),
            (
                "occluder_interior_region",
                json!({
                    "x": region.x,
                    "y": region.y,
                    "width": region.width,
                    "height": region.height
                }),
            ),
        ]);
        if helper_pixels <= tolerance {
            checks.push(
                checked_check(
                    id,
                    "helper_render_layer",
                    "helper_layer_occluded_by_subject",
                    target_id,
                    affected,
                    observed,
                    (
                        "helper pixels are absent from the occluder interior, so depth/layer policy is preserved",
                        "no action needed",
                    ),
                )
                .with_region_from_screen("node", first_handle(&occluder_handles), region),
            );
        } else {
            checks.push(
                error_check(
                    id,
                    "helper_render_layer",
                    "helper_layer_overdraws_subject",
                    target_id,
                    affected,
                    observed,
                    (
                        "helper-colored pixels appear inside the occluder interior",
                        "move the helper behind the subject, keep depth-tested helpers out of front layers, or remove the helper occlusion expectation if the overdraw is intentional",
                    ),
                )
                .with_region_from_screen("node", first_handle(&occluder_handles), region),
            );
        }
    }
    checks
}

fn occluder_interior_region(
    capture: &CaptureRgba8,
    inspection: &SceneInspectionReportV1,
    handles: &[u64],
) -> Option<CaptureScreenRegion> {
    let mut rects = Vec::new();
    for handle in handles {
        let draws = draws_for_handle(inspection, *handle);
        if let Some(rect) = projected_node_rect(capture, draws.as_slice()) {
            rects.push(rect);
        }
    }
    let rect = union_rects(rects.as_slice())?;
    shrink_region(
        clipped_region_from_rect(capture, rect)?,
        OCCLUDER_INTERIOR_INSET_PX,
    )
}

fn helper_srgb8_color(inspection: &SceneInspectionReportV1, handles: &[u64]) -> Option<[u8; 4]> {
    handles.iter().find_map(|handle| {
        draws_for_handle(inspection, *handle)
            .into_iter()
            .find_map(|draw| draw.material.as_ref().map(|material| material.base_color))
            .map(linear_rgba_to_srgb8)
    })
}

fn count_helper_pixels_in_region(
    capture: &CaptureRgba8,
    region: CaptureScreenRegion,
    helper_color: [u8; 4],
) -> u64 {
    let mut count = 0_u64;
    for y in region.y..region.y.saturating_add(region.height) {
        for x in region.x..region.x.saturating_add(region.width) {
            let offset = ((y as usize) * (capture.descriptor.width as usize) + (x as usize)) * 4;
            let Some(pixel) = capture.rgba8.get(offset..offset + 4) else {
                continue;
            };
            if color_matches(pixel, helper_color) {
                count = count.saturating_add(1);
            }
        }
    }
    count
}

fn color_matches(pixel: &[u8], helper_color: [u8; 4]) -> bool {
    pixel
        .iter()
        .zip(helper_color)
        .take(3)
        .all(|(sample, target)| sample.abs_diff(target) <= DEFAULT_HELPER_COLOR_TOLERANCE)
}

fn shrink_region(region: CaptureScreenRegion, inset: u32) -> Option<CaptureScreenRegion> {
    if region.width <= inset.saturating_mul(2) || region.height <= inset.saturating_mul(2) {
        return None;
    }
    Some(CaptureScreenRegion {
        x: region.x.saturating_add(inset),
        y: region.y.saturating_add(inset),
        width: region.width.saturating_sub(inset.saturating_mul(2)),
        height: region.height.saturating_sub(inset.saturating_mul(2)),
    })
}

fn union_rects(rects: &[crate::CaptureScreenRect]) -> Option<crate::CaptureScreenRect> {
    let mut min_x = f32::INFINITY;
    let mut min_y = f32::INFINITY;
    let mut max_x = f32::NEG_INFINITY;
    let mut max_y = f32::NEG_INFINITY;
    for rect in rects {
        min_x = min_x.min(rect.min_x);
        min_y = min_y.min(rect.min_y);
        max_x = max_x.max(rect.max_x);
        max_y = max_y.max(rect.max_y);
    }
    if !min_x.is_finite() || !min_y.is_finite() || !max_x.is_finite() || !max_y.is_finite() {
        return None;
    }
    Some(crate::CaptureScreenRect {
        min_x,
        min_y,
        max_x,
        max_y,
        width: (max_x - min_x).max(0.0),
        height: (max_y - min_y).max(0.0),
        center_x: (min_x + max_x) * 0.5,
        center_y: (min_y + max_y) * 0.5,
    })
}

fn combined_handles(left: &[u64], right: &[u64]) -> Vec<u64> {
    let mut handles = left.to_vec();
    handles.extend(right);
    handles.sort_unstable();
    handles.dedup();
    handles
}

fn first_handle(handles: &[u64]) -> Option<u64> {
    handles.first().copied()
}