scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use scena::{
    Color, PerspectiveCamera, Primitive, ReferenceImage, ReferenceImageError,
    ReferenceImageTolerance, Renderer, Scene, Transform, Vec3, Vertex, regress,
    regress_with_tolerance,
};

#[test]
fn reference_image_regression_accepts_exact_rgba8_match() {
    let expected = ReferenceImage::from_rgba8(2, 1, vec![10, 20, 30, 255, 40, 50, 60, 255])
        .expect("expected reference image is valid");
    let actual = ReferenceImage::from_rgba8(2, 1, vec![10, 20, 30, 255, 40, 50, 60, 255])
        .expect("actual reference image is valid");

    let report = regress(&actual, &expected).expect("exact image match passes");

    assert!(report.passed());
    assert_eq!(report.total_pixels(), 2);
    assert_eq!(report.mismatched_pixels(), 0);
    assert_eq!(report.max_abs_diff(), 0);
}

#[test]
fn reference_image_regression_reports_tolerance_failure() {
    let expected = ReferenceImage::from_rgba8(2, 1, vec![10, 20, 30, 255, 40, 50, 60, 255])
        .expect("expected reference image is valid");
    let actual = ReferenceImage::from_rgba8(2, 1, vec![10, 20, 31, 255, 44, 50, 60, 255])
        .expect("actual reference image is valid");

    let error = regress_with_tolerance(
        &actual,
        &expected,
        ReferenceImageTolerance::new()
            .with_max_abs_diff(2)
            .with_max_mismatched_pixels(0),
    )
    .expect_err("second pixel exceeds the max channel tolerance");

    let ReferenceImageError::DiffExceeded(report) = error else {
        panic!("expected a diff-exceeded report");
    };
    assert!(!report.passed());
    assert_eq!(report.mismatched_pixels(), 1);
    assert_eq!(report.max_abs_diff(), 4);
    assert_eq!(report.tolerance().max_abs_diff(), 2);
}

#[test]
fn reference_image_regression_compares_real_renderer_output_to_committed_golden() {
    let mut scene = Scene::new();
    let camera = scene
        .add_perspective_camera(
            scene.root(),
            PerspectiveCamera::default(),
            Transform::at(Vec3::new(0.0, 0.0, 2.0)),
        )
        .expect("camera inserts");
    scene
        .set_active_camera(camera)
        .expect("camera becomes active");
    let color = Color::from_srgb_u8(80, 140, 220);
    scene
        .add_renderable(
            scene.root(),
            vec![Primitive::triangle([
                Vertex {
                    position: Vec3::new(-2.0, -2.0, 0.0),
                    color,
                },
                Vertex {
                    position: Vec3::new(4.0, -2.0, 0.0),
                    color,
                },
                Vertex {
                    position: Vec3::new(-2.0, 4.0, 0.0),
                    color,
                },
            ])],
            Transform::default(),
        )
        .expect("fullscreen triangle inserts");

    let mut renderer = Renderer::headless(4, 4).expect("headless renderer builds");
    renderer.prepare(&mut scene).expect("scene prepares");
    renderer.render(&scene, camera).expect("scene renders");
    let actual = ReferenceImage::from_rgba8(4, 4, renderer.frame_rgba8().to_vec())
        .expect("renderer output is rgba8");
    let expected = ReferenceImage::from_rgba8(
        4,
        4,
        vec![
            56, 130, 214, 255, 56, 130, 214, 255, 56, 130, 214, 255, 0, 0, 0, 255, 56, 130, 214,
            255, 56, 130, 214, 255, 56, 130, 214, 255, 56, 130, 214, 255, 56, 130, 214, 255, 56,
            130, 214, 255, 56, 130, 214, 255, 56, 130, 214, 255, 56, 130, 214, 255, 56, 130, 214,
            255, 56, 130, 214, 255, 56, 130, 214, 255,
        ],
    )
    .expect("committed renderer golden is rgba8");

    let report = regress(&actual, &expected).expect("renderer output matches committed golden");
    assert_eq!(report.total_pixels(), 16);
    assert_eq!(report.mismatched_pixels(), 0);
}

#[test]
fn reference_image_regression_rejects_invalid_rgba_length() {
    let error =
        ReferenceImage::from_rgba8(2, 2, vec![0, 0, 0, 255]).expect_err("length is invalid");

    assert!(matches!(
        error,
        ReferenceImageError::InvalidRgbaLength {
            width: 2,
            height: 2,
            expected_len: 16,
            actual_len: 4,
        }
    ));
}

#[test]
fn reference_image_regression_rejects_dimension_mismatch() {
    let expected =
        ReferenceImage::from_rgba8(1, 2, vec![10, 20, 30, 255, 40, 50, 60, 255]).unwrap();
    let actual = ReferenceImage::from_rgba8(2, 1, vec![10, 20, 30, 255, 40, 50, 60, 255]).unwrap();

    let error = regress(&actual, &expected).expect_err("dimensions differ");

    assert!(matches!(
        error,
        ReferenceImageError::DimensionMismatch {
            actual_width: 2,
            actual_height: 1,
            expected_width: 1,
            expected_height: 2,
        }
    ));
}