scena 1.3.0

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use scena::{Aabb, FramingOptions, PerspectiveCamera, Scene, Transform, Vec3};

fn unit_bounds() -> Aabb {
    Aabb::new(Vec3::new(-0.5, -0.5, -0.5), Vec3::new(0.5, 0.5, 0.5))
}

fn direction_for(options: FramingOptions) -> Vec3 {
    let width = 800;
    let height = 600;
    let mut scene = Scene::new();
    let camera = scene
        .add_perspective_camera(
            scene.root(),
            PerspectiveCamera::default().with_aspect(width as f32 / height as f32),
            Transform::default(),
        )
        .expect("camera inserts");
    let outcome = scene
        .frame_bounds(
            camera,
            unit_bounds(),
            options.fill(0.8).margin_px(16.0).viewport(width, height),
        )
        .expect("framing succeeds");
    (outcome.camera_transform.translation - outcome.target).normalize()
}

fn assert_vec_close(actual: Vec3, expected: Vec3, tolerance: f32) {
    let delta = actual - expected;
    assert!(
        delta.length() <= tolerance,
        "expected {expected:?}, got {actual:?}, delta={delta:?}"
    );
}

#[test]
fn named_cardinal_views_match_world_axes() {
    assert_vec_close(
        direction_for(FramingOptions::new().front()),
        Vec3::new(0.0, 0.0, 1.0),
        1e-5,
    );
    assert_vec_close(
        direction_for(FramingOptions::new().back()),
        Vec3::new(0.0, 0.0, -1.0),
        1e-5,
    );
    assert_vec_close(
        direction_for(FramingOptions::new().right()),
        Vec3::new(1.0, 0.0, 0.0),
        1e-5,
    );
    assert_vec_close(
        direction_for(FramingOptions::new().left()),
        Vec3::new(-1.0, 0.0, 0.0),
        1e-5,
    );
    assert_vec_close(
        direction_for(FramingOptions::new().top()),
        Vec3::new(0.0, 1.0, 0.0),
        1e-5,
    );
    assert_vec_close(
        direction_for(FramingOptions::new().bottom()),
        Vec3::new(0.0, -1.0, 0.0),
        1e-5,
    );
}

#[test]
fn three_quarter_front_views_mirror_across_yz_plane() {
    let right = direction_for(FramingOptions::new().three_quarter_front_right());
    let left = direction_for(FramingOptions::new().three_quarter_front_left());

    assert!((right.x + left.x).abs() <= 1e-5, "{right:?} {left:?}");
    assert!((right.y - left.y).abs() <= 1e-5, "{right:?} {left:?}");
    assert!((right.z - left.z).abs() <= 1e-5, "{right:?} {left:?}");
}

#[test]
fn azimuth_elevation_matches_cardinal_presets() {
    assert_vec_close(
        direction_for(FramingOptions::new().azimuth_elevation(0.0, 0.0)),
        direction_for(FramingOptions::new().front()),
        1e-5,
    );
    assert_vec_close(
        direction_for(FramingOptions::new().azimuth_elevation(90.0, 0.0)),
        direction_for(FramingOptions::new().right()),
        1e-5,
    );
    assert_vec_close(
        direction_for(FramingOptions::new().azimuth_elevation(0.0, 90.0)),
        direction_for(FramingOptions::new().top()),
        1e-5,
    );
    assert_vec_close(
        direction_for(FramingOptions::new().azimuth_elevation(-90.0, 0.0)),
        direction_for(FramingOptions::new().left()),
        1e-5,
    );
    assert_vec_close(
        direction_for(FramingOptions::new().azimuth_elevation(180.0, 0.0)),
        direction_for(FramingOptions::new().back()),
        1e-5,
    );
}

#[test]
fn azimuth_elevation_clamps_elevation_to_vertical_axes() {
    assert_vec_close(
        direction_for(FramingOptions::new().azimuth_elevation(0.0, 120.0)),
        direction_for(FramingOptions::new().top()),
        1e-5,
    );
    assert_vec_close(
        direction_for(FramingOptions::new().azimuth_elevation(0.0, -120.0)),
        direction_for(FramingOptions::new().bottom()),
        1e-5,
    );
}

#[test]
fn azimuth_elevation_decodes_approved_connector_angle() {
    assert_vec_close(
        direction_for(FramingOptions::new().azimuth_elevation(-27.5, 17.8)),
        Vec3::new(-0.4398, 0.3051, 0.8447),
        1e-3,
    );
}

#[test]
fn frame_bounds_right_preset_places_camera_to_right_of_bounds_center() {
    let width = 800;
    let height = 600;
    let bounds = Aabb::new(Vec3::new(2.0, -1.0, -0.5), Vec3::new(4.0, 1.0, 0.5));
    let mut scene = Scene::new();
    let camera = scene
        .add_perspective_camera(
            scene.root(),
            PerspectiveCamera::default().with_aspect(width as f32 / height as f32),
            Transform::default(),
        )
        .expect("camera inserts");

    let outcome = scene
        .frame_bounds(
            camera,
            bounds,
            FramingOptions::new()
                .right()
                .fill(0.8)
                .margin_px(16.0)
                .viewport(width, height),
        )
        .expect("right framing succeeds");

    assert_vec_close(outcome.target, bounds.center(), 1e-5);
    assert!(
        outcome.camera_transform.translation.x > outcome.target.x,
        "{outcome:?}"
    );
    assert!(
        (outcome.camera_transform.translation.y - outcome.target.y).abs() <= 1e-5,
        "{outcome:?}"
    );
    assert!(
        (outcome.camera_transform.translation.z - outcome.target.z).abs() <= 1e-5,
        "{outcome:?}"
    );
}