sim-lib-view-spatial 0.1.0

Spatial glasses surface encoding over the shared SIM Web Scene and device adapters.
Documentation
use std::rc::Rc;

use sim_kernel::Expr;
use sim_lib_scene::{Anchor, AnchorSpace, Transform3};
use sim_lib_view_device::{
    AckChannel, DeviceSurfaceCapsExt, EncodedScene, GlanceInput, GlanceState, LocalAdapter,
};
use sim_value::{access, build};

use crate::{PoseView, Reprojector, halo_glance_config};

#[test]
fn reprojector_bespoke_halo_uses_glance_adapter() {
    let encoded_spatial = EncodedScene::new(spatial_scene());
    let viture = sim_lib_view::SurfaceCaps::from_preset("glasses-luma-ultra", "viture")
        .unwrap()
        .device_profile();
    let reprojector = Reprojector::new(12);
    let encode_count = 1;

    let first = reprojector
        .adapt(
            &encoded_spatial,
            &PoseView::identity(1)
                .with_timing(1, 40_000_000)
                .with_translation([0.1, 0.0, 0.0])
                .with_angles(8.0, 0.0, 0.0),
            &viture,
        )
        .unwrap();
    let second = reprojector
        .adapt(
            &encoded_spatial,
            &PoseView::identity(2)
                .with_timing(2, 4_000_000)
                .with_translation([0.0, 0.0, 0.0]),
            &viture,
        )
        .unwrap();
    assert_scene_kind(first.as_ref(), "stereo");
    assert_scene_kind(second.as_ref(), "stereo");
    assert_ne!(first, second);
    assert_eq!(stereo_children(first.as_ref(), "left-eye").len(), 2);
    assert_eq!(stereo_children(first.as_ref(), "right-eye").len(), 2);
    assert_anchor_rule(first.as_ref(), "world-locked");
    assert_anchor_rule(first.as_ref(), "head-locked");
    assert_eq!(encode_count, 1);

    let mirror_profile = sim_lib_view::SurfaceCaps::from_preset("glasses-stereo", "mirror")
        .unwrap()
        .device_profile();
    let mirrored = reprojector
        .adapt(&encoded_spatial, &PoseView::identity(3), &mirror_profile)
        .unwrap();
    assert!(Rc::ptr_eq(&mirrored, &encoded_spatial.shared()));

    let halo = halo_glance_config();
    assert_eq!(halo.budget.ack, AckChannel::GlyphFlash);
    assert_eq!(halo.budget.cells, 6);
    assert_eq!(halo.budget.glyphs, 128);

    let encoded_glance =
        EncodedScene::new(sim_lib_scene::glance_card("Halo", None, None, "info", 1));
    let halo_profile = sim_lib_view::SurfaceCaps::from_preset("glasses-hud", "halo")
        .unwrap()
        .device_profile();
    let tapped = halo
        .adapt(
            &encoded_glance,
            &GlanceState::with_input(GlanceInput::Tap, 7),
            &halo_profile,
        )
        .unwrap();
    let tapped_again = halo
        .adapt(
            &encoded_glance,
            &GlanceState::with_input(GlanceInput::Tap, 8),
            &halo_profile,
        )
        .unwrap();
    assert_scene_kind(tapped.as_ref(), "glance");
    assert_scene_kind(tapped_again.as_ref(), "glance");
    assert_ne!(tapped, tapped_again);
    assert_eq!(
        access::field_sym(tapped.as_ref(), "ack-channel")
            .unwrap()
            .name
            .as_ref(),
        "glyph-flash"
    );
    assert_eq!(
        access::field_sym(tapped.as_ref(), "ack-input")
            .unwrap()
            .name
            .as_ref(),
        "tap"
    );
    assert_eq!(field_u64(tapped.as_ref(), "ack-ms"), Some(120));
    assert_eq!(field_u64(tapped.as_ref(), "ack-tick"), Some(7));
    assert_eq!(field_u64(tapped_again.as_ref(), "ack-tick"), Some(8));
    assert_eq!(encode_count, 1);
}

fn spatial_scene() -> Expr {
    sim_lib_scene::spatial(vec![
        sim_lib_scene::panel(
            "world",
            sim_lib_scene::node("text", vec![("text", build::text("World"))]),
            Anchor::new(AnchorSpace::World, "desk"),
            Transform3::new([0.0, 0.0, -1.6], [0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0]),
        ),
        sim_lib_scene::panel(
            "head",
            sim_lib_scene::node("text", vec![("text", build::text("Head"))]),
            Anchor::new(AnchorSpace::Head, "view"),
            Transform3::identity(),
        ),
        sim_lib_scene::panel(
            "culled",
            sim_lib_scene::node("text", vec![("text", build::text("Hidden"))]),
            Anchor::new(AnchorSpace::World, "side"),
            Transform3::new([5.0, 0.0, -1.0], [0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0]),
        ),
    ])
}

fn assert_scene_kind(scene: &Expr, expected: &str) {
    let kind = sim_lib_scene::node_kind(scene).expect("scene kind");
    assert_eq!(kind.namespace.as_deref(), Some("scene"));
    assert_eq!(kind.name.as_ref(), expected);
}

fn stereo_children<'a>(scene: &'a Expr, eye_field: &str) -> &'a [Expr] {
    let eye = access::field(scene, eye_field).expect("eye field");
    match access::field(eye, "children").expect("children") {
        Expr::List(children) => children,
        other => panic!("children must be list, got {other:?}"),
    }
}

fn assert_anchor_rule(scene: &Expr, expected: &str) {
    let found = ["left-eye", "right-eye"].into_iter().any(|eye| {
        stereo_children(scene, eye).iter().any(|child| {
            access::field_sym(child, "anchor-rule")
                .is_some_and(|symbol| symbol.name.as_ref() == expected)
        })
    });
    assert!(found, "missing anchor rule {expected}");
}

fn field_u64(scene: &Expr, name: &str) -> Option<u64> {
    let Expr::Number(number) = access::field(scene, name)? else {
        return None;
    };
    number.canonical.parse().ok()
}