scena 1.7.2

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

use scena::{
    Aabb, CameraBookmark, CameraState, FramingOptions, OrbitControls, PerspectiveCamera, Scene,
    Transform, TransitionEasing, Vec3,
};

fn assert_close(actual: f32, expected: f32) {
    assert!(
        (actual - expected).abs() <= 1.0e-4,
        "expected {actual} to be close to {expected}"
    );
}

fn assert_vec_close(actual: Vec3, expected: Vec3) {
    assert_close(actual.x, expected.x);
    assert_close(actual.y, expected.y);
    assert_close(actual.z, expected.z);
}

fn assert_state_close(actual: CameraState, expected: CameraState) {
    assert_vec_close(actual.target, expected.target);
    assert_close(actual.distance, expected.distance);
    assert_close(actual.yaw_radians, expected.yaw_radians);
    assert_close(actual.pitch_radians, expected.pitch_radians);
}

#[test]
fn camera_bookmark_from_framing_preserves_state_bounds_and_description() {
    let mut scene = Scene::new();
    let camera = scene
        .add_perspective_camera(
            scene.root(),
            PerspectiveCamera::standard(),
            Transform::IDENTITY,
        )
        .expect("camera inserts");
    let bounds = Aabb::new(Vec3::new(-1.0, -0.5, -0.25), Vec3::new(1.0, 0.5, 0.25));
    let framing = scene
        .frame_bounds(
            camera,
            bounds,
            FramingOptions::new()
                .azimuth_elevation(-32.0, 14.0)
                .viewport(1280, 720),
        )
        .expect("bounds frame");

    let bookmark = CameraBookmark::from_framing("pump_detail", framing)
        .with_target_bounds(bounds)
        .with_description("service-side pump detail");

    assert_eq!(bookmark.name(), "pump_detail");
    assert_eq!(bookmark.description(), Some("service-side pump detail"));
    assert_eq!(bookmark.target_bounds(), Some(bounds));
    assert_state_close(bookmark.state(), CameraState::from_framing(framing));
}

#[test]
fn orbit_controls_fly_to_samples_shortest_path_and_zero_duration() {
    let start = OrbitControls::new(Vec3::ZERO, 4.0).with_angles(PI - 0.1, 0.0);
    let target = CameraState {
        target: Vec3::new(1.0, 2.0, 3.0),
        distance: 6.0,
        yaw_radians: -PI + 0.1,
        pitch_radians: 0.2,
    };

    let mut fly_to = start
        .fly_to(target, TransitionEasing::Linear, 1.0)
        .expect("fly-to builds");
    let halfway = fly_to.advance(0.5);

    assert!(!fly_to.is_complete());
    assert_vec_close(halfway.target(), Vec3::new(0.5, 1.0, 1.5));
    assert_close(halfway.distance(), 5.0);
    assert!(
        (halfway.yaw_radians() - PI).abs() <= 1.0e-4,
        "fly-to should cross the +/-pi wrap on the short arc, got {}",
        halfway.yaw_radians()
    );
    assert_close(halfway.pitch_radians(), 0.1);

    let final_controls = fly_to.advance(0.5);
    assert!(fly_to.is_complete());
    assert_state_close(final_controls.camera_state(), target);

    let zero = start
        .fly_to(target, TransitionEasing::EaseInOut, 0.0)
        .expect("zero-duration fly-to builds");
    assert!(zero.is_complete());
    assert_state_close(zero.sample().camera_state(), target);

    let invalid = start
        .fly_to(
            CameraState {
                distance: 0.0,
                ..target
            },
            TransitionEasing::Linear,
            1.0,
        )
        .expect_err("invalid target state is rejected");
    assert!(invalid.to_string().contains("distance"));
}

#[cfg(feature = "scene-host")]
#[test]
fn scene_host_camera_bookmark_delegates_through_camera_eased_patch() {
    use scena::{
        SceneHostCameraState, SceneHostCore, SceneHostEasing, SceneHostErrorCode,
        VisualPatchResultV1,
    };

    let mut host = SceneHostCore::headless(96, 64).expect("host builds");
    let start = SceneHostCameraState {
        target: Vec3::ZERO,
        distance: 4.0,
        yaw_radians: 0.0,
        pitch_radians: 0.0,
    };
    let target = SceneHostCameraState {
        target: Vec3::new(2.0, 0.0, 0.0),
        distance: 8.0,
        yaw_radians: 1.0,
        pitch_radians: 0.25,
    };
    host.set_camera(start).expect("camera start sets");

    let bookmark = CameraBookmark::new("detail", target);
    let scheduled = host
        .set_camera_bookmark(&bookmark, 2.0, SceneHostEasing::Linear)
        .expect("bookmark schedules through visual patch");
    assert_eq!(scheduled.applied.camera_eased, 1);
    assert!(scheduled.failed.is_empty());
    assert_state_close(host.get_camera(), start);

    host.advance(1.0).expect("transition advances halfway");
    assert_state_close(
        host.get_camera(),
        SceneHostCameraState {
            target: Vec3::new(1.0, 0.0, 0.0),
            distance: 6.0,
            yaw_radians: 0.5,
            pitch_radians: 0.125,
        },
    );

    let json = serde_json::to_string(&bookmark).expect("bookmark serializes");
    let result_json = host
        .set_camera_bookmark_json(&json, 0.0, SceneHostEasing::Linear)
        .expect("bookmark JSON applies");
    let result: VisualPatchResultV1 =
        serde_json::from_str(&result_json).expect("bookmark result JSON parses");
    assert_eq!(result.applied.camera_eased, 1);
    assert_state_close(host.get_camera(), target);

    let bad = CameraBookmark::new(
        "bad",
        SceneHostCameraState {
            distance: 0.0,
            ..target
        },
    );
    let invalid_result = host
        .set_camera_bookmark(&bad, 0.25, SceneHostEasing::Linear)
        .expect("invalid bookmark camera state is reported per patch entry");
    assert_eq!(invalid_result.failed.len(), 1);
    assert_eq!(invalid_result.failed[0].channel, "camera_eased");
    assert_eq!(
        invalid_result.failed[0].code,
        SceneHostErrorCode::InvalidInput
    );
}

#[test]
fn viewer_helpers_store_optional_camera_bookmarks() {
    let overview = CameraBookmark::new(
        "overview",
        CameraState {
            target: Vec3::ZERO,
            distance: 4.0,
            yaw_radians: 0.0,
            pitch_radians: 0.0,
        },
    );
    let detail = CameraBookmark::new(
        "detail",
        CameraState {
            target: Vec3::new(1.0, 0.0, 0.0),
            distance: 2.0,
            yaw_radians: 0.5,
            pitch_radians: 0.2,
        },
    );

    let viewer = pollster::block_on(
        scena::headless_gltf_viewer("tests/assets/gltf/mesh_material_vertex_color_scene.gltf")
            .with_camera_bookmark(overview.clone())
            .with_camera_bookmarks([detail.clone()])
            .build(),
    )
    .expect("viewer builds");

    assert_eq!(viewer.camera_bookmarks(), &[overview, detail]);
}