viewport-lib 0.18.2

3D viewport rendering library
Documentation
//! Integration tests for runtime camera command writeback.

use viewport_lib::camera::camera::{Camera, CameraTarget};
use viewport_lib::interaction::input::ActionFrame;
use viewport_lib::interaction::selection::Selection;
use viewport_lib::runtime::{
    CameraCommand, CameraFollowTarget, RuntimeFrameContext, RuntimeOutput, RuntimePlugin,
    RuntimeStepContext, ViewportRuntime, apply_camera_commands, plugin::phase,
};
use viewport_lib::scene::scene::Scene;

fn make_frame(camera: &Camera, input: &ActionFrame) -> RuntimeFrameContext {
    let mut frame = RuntimeFrameContext::default();
    frame.dt = 1.0 / 60.0;
    frame.camera = camera.clone();
    frame.viewport_size = glam::Vec2::new(800.0, 600.0);
    frame.input = input.clone();
    frame
}

fn step(runtime: &mut ViewportRuntime, camera: &Camera) -> RuntimeOutput {
    let input = ActionFrame::default();
    let mut scene = Scene::new();
    let mut sel = Selection::new();
    runtime.step(&mut scene, &mut sel, &make_frame(camera, &input))
}

// --- Plugins -----------------------------------------------------------------

struct SetCenterPlugin(glam::Vec3);

impl RuntimePlugin for SetCenterPlugin {
    fn priority(&self) -> i32 {
        phase::ANIMATE
    }
    fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
        ctx.output.events.emit(CameraCommand::SetCenter(self.0));
    }
}

struct OffsetCenterPlugin(glam::Vec3);

impl RuntimePlugin for OffsetCenterPlugin {
    fn priority(&self) -> i32 {
        phase::POST_SIM
    }
    fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
        ctx.output.events.emit(CameraCommand::OffsetCenter(self.0));
    }
}

struct SetDistancePlugin(f32);

impl RuntimePlugin for SetDistancePlugin {
    fn priority(&self) -> i32 {
        phase::ANIMATE
    }
    fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
        ctx.output.events.emit(CameraCommand::SetDistance(self.0));
    }
}

struct BlendPlugin {
    target: CameraTarget,
    weight: f32,
}

impl RuntimePlugin for BlendPlugin {
    fn priority(&self) -> i32 {
        phase::ANIMATE
    }
    fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
        ctx.output.events.emit(CameraCommand::BlendToward {
            target: self.target,
            weight: self.weight,
        });
    }
}

// --- Tests -------------------------------------------------------------------

#[test]
fn set_center_applies_to_camera() {
    let mut runtime =
        ViewportRuntime::new().with_plugin(SetCenterPlugin(glam::Vec3::new(1.0, 2.0, 3.0)));
    let camera = Camera::default();
    let mut output = step(&mut runtime, &camera);

    let mut cam = camera.clone();
    apply_camera_commands(&mut output.events, &mut cam);
    assert!((cam.center - glam::Vec3::new(1.0, 2.0, 3.0)).length() < 1e-5);
}

#[test]
fn offset_center_adds_to_current_position() {
    let mut camera = Camera::default();
    camera.center = glam::Vec3::new(5.0, 0.0, 0.0);

    let mut runtime =
        ViewportRuntime::new().with_plugin(OffsetCenterPlugin(glam::Vec3::new(1.0, 0.0, 0.0)));
    let mut output = step(&mut runtime, &camera);

    apply_camera_commands(&mut output.events, &mut camera);
    assert!((camera.center.x - 6.0).abs() < 1e-5);
}

#[test]
fn set_distance_applies() {
    let mut runtime = ViewportRuntime::new().with_plugin(SetDistancePlugin(10.0));
    let camera = Camera::default();
    let mut output = step(&mut runtime, &camera);

    let mut cam = camera.clone();
    apply_camera_commands(&mut output.events, &mut cam);
    assert!((cam.distance - 10.0).abs() < 1e-5);
}

#[test]
fn blend_toward_interpolates() {
    let target = CameraTarget {
        center: glam::Vec3::new(10.0, 0.0, 0.0),
        distance: 20.0,
        orientation: glam::Quat::IDENTITY,
    };
    let mut runtime = ViewportRuntime::new().with_plugin(BlendPlugin {
        target,
        weight: 0.5,
    });

    let mut camera = Camera::default();
    camera.center = glam::Vec3::ZERO;
    camera.distance = 4.0;

    let mut output = step(&mut runtime, &camera);
    apply_camera_commands(&mut output.events, &mut camera);

    assert!((camera.center.x - 5.0).abs() < 1e-4);
    assert!((camera.distance - 12.0).abs() < 1e-4);
}

#[test]
fn blend_at_weight_one_snaps_to_target() {
    let target = CameraTarget {
        center: glam::Vec3::new(7.0, 8.0, 9.0),
        distance: 5.0,
        orientation: glam::Quat::IDENTITY,
    };
    let mut runtime = ViewportRuntime::new().with_plugin(BlendPlugin {
        target,
        weight: 1.0,
    });

    let camera = Camera::default();
    let mut output = step(&mut runtime, &camera);

    let mut cam = camera.clone();
    apply_camera_commands(&mut output.events, &mut cam);
    assert!((cam.center - glam::Vec3::new(7.0, 8.0, 9.0)).length() < 1e-5);
    assert!((cam.distance - 5.0).abs() < 1e-5);
}

#[test]
fn commands_applied_in_emission_order() {
    let mut runtime = ViewportRuntime::new()
        .with_plugin(SetCenterPlugin(glam::Vec3::new(10.0, 0.0, 0.0)))
        .with_plugin(OffsetCenterPlugin(glam::Vec3::new(1.0, 0.0, 0.0)));

    let camera = Camera::default();
    let mut output = step(&mut runtime, &camera);
    assert_eq!(output.events.read::<CameraCommand>().count(), 2);

    let mut cam = camera.clone();
    apply_camera_commands(&mut output.events, &mut cam);
    assert!((cam.center.x - 11.0).abs() < 1e-5);
}

#[test]
fn no_commands_leaves_camera_unchanged() {
    let mut runtime = ViewportRuntime::new();
    let camera = Camera::default();
    let original_center = camera.center;
    let original_distance = camera.distance;
    let mut output = step(&mut runtime, &camera);

    let mut cam = camera.clone();
    apply_camera_commands(&mut output.events, &mut cam);
    assert_eq!(cam.center, original_center);
    assert_eq!(cam.distance, original_distance);
}

#[test]
fn camera_commands_cleared_each_frame() {
    let mut runtime = ViewportRuntime::new().with_plugin(SetCenterPlugin(glam::Vec3::ONE));
    let camera = Camera::default();
    step(&mut runtime, &camera);
    let output2 = step(&mut runtime, &camera);
    assert_eq!(output2.events.read::<CameraCommand>().count(), 1);
}

#[test]
fn camera_follow_target_independent_of_commands() {
    let mut runtime = ViewportRuntime::new().with_plugin(SetCenterPlugin(glam::Vec3::ONE));
    let camera = Camera::default();
    let output = step(&mut runtime, &camera);
    assert_eq!(output.events.read::<CameraFollowTarget>().count(), 0);
    assert_eq!(output.events.read::<CameraCommand>().count(), 1);
}

#[test]
fn default_output_fields_empty() {
    let mut runtime = ViewportRuntime::new().with_plugin(SetCenterPlugin(glam::Vec3::ZERO));
    let camera = Camera::default();
    let output = step(&mut runtime, &camera);
    assert!(output.selection_ops.is_empty());
    assert!(output.node_transform_ops.is_empty());
}

#[test]
fn set_orientation_applies() {
    let q = glam::Quat::from_rotation_y(std::f32::consts::FRAC_PI_2);
    struct SetOrientationPlugin(glam::Quat);
    impl RuntimePlugin for SetOrientationPlugin {
        fn priority(&self) -> i32 {
            phase::ANIMATE
        }
        fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
            ctx.output
                .events
                .emit(CameraCommand::SetOrientation(self.0));
        }
    }

    let mut runtime = ViewportRuntime::new().with_plugin(SetOrientationPlugin(q));
    let camera = Camera::default();
    let mut output = step(&mut runtime, &camera);

    let mut cam = camera.clone();
    apply_camera_commands(&mut output.events, &mut cam);
    assert!(cam.orientation.dot(q).abs() > 0.9999);
}