use super::*;
use crate::components::{Camera3D, InputKey};
use crate::test_support::isolate_state_dir;
const VP: [f32; 2] = [1280.0, 720.0];
fn hook() -> EditorHook {
EditorHook::new("unused.jsonl".to_string(), Vec::new())
}
fn camera_world(pos: [f32; 3]) -> (World, AssetId) {
crate::ecs::asset_id::reset_interner();
let id = crate::ecs::asset_id::intern("box");
let mut world = World::new();
world.add_component(Camera3D {
position: pos,
view_matrix: concinnity_core::gfx::camera::view_matrix(pos, 0.0, 0.0),
fov_y_degrees: 90.0,
near: 0.05,
far: 200.0,
yaw: 0.0,
pitch: 0.0,
desired_move: [0.0; 3],
jump_requested: false,
interact_requested: false,
controller: None,
});
world.insert_resource(crate::ecs::PickIndex {
entries: vec![crate::ecs::PickEntry {
asset_id: id,
bb_min: [-1.0, -1.0, -1.0],
bb_max: [1.0, 1.0, 1.0],
}],
});
(world, id)
}
fn camera(world: &World) -> &Camera3D {
world.query::<Camera3D>().next().expect("camera")
}
fn finish_glide(h: &mut EditorHook, world: &mut World) {
let glide = h.glide.as_mut().expect("a glide is armed");
glide.start -= std::time::Duration::from_secs(1);
h.drive_glide(
&FrameInput {
viewport: VP,
..Default::default()
},
world,
);
}
fn alt_press(mouse: [f32; 2]) -> FrameInput {
FrameInput {
viewport: VP,
mouse_x: mouse[0],
mouse_y: mouse[1],
alt: true,
left_button_down: true,
..Default::default()
}
}
fn drag_to(mouse: [f32; 2]) -> FrameInput {
FrameInput {
viewport: VP,
mouse_x: mouse[0],
mouse_y: mouse[1],
left_button_down: true,
..Default::default()
}
}
#[test]
fn alt_drag_tumbles_the_camera_around_the_selection() {
let (mut world, _) = camera_world([0.0, 0.0, 10.0]);
let mut h = hook();
h.selection.set(vec!["box".to_string()]);
assert!(h.try_begin_orbit(&alt_press([600.0, 400.0]), VP, &world));
assert!(h.orbit.is_some(), "the tumble is armed");
let before = camera(&world).position;
h.drive_orbit(&drag_to([700.0, 400.0]), &mut world);
let after = camera(&world).position;
assert_ne!(before, after, "the drag moved the camera");
let radius = |p: [f32; 3]| (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
assert!(
(radius(before) - radius(after)).abs() < 0.01,
"tumbling kept the distance to the pivot: {before:?} -> {after:?}"
);
assert_eq!(
camera(&world).view_matrix,
concinnity_core::gfx::camera::view_matrix(after, camera(&world).yaw, camera(&world).pitch)
);
}
#[test]
fn releasing_the_button_ends_the_tumble() {
let (mut world, _) = camera_world([0.0, 0.0, 10.0]);
let mut h = hook();
h.selection.set(vec!["box".to_string()]);
assert!(h.try_begin_orbit(&alt_press([600.0, 400.0]), VP, &world));
let released = FrameInput {
viewport: VP,
mouse_x: 700.0,
mouse_y: 400.0,
..Default::default()
};
h.drive_orbit(&released, &mut world);
assert!(h.orbit.is_none(), "the release disarmed the tumble");
}
#[test]
fn a_tumble_is_declined_without_a_viewport_press_and_a_pivot() {
let (world, _) = camera_world([0.0, 0.0, 10.0]);
let mut empty = hook();
assert!(
!empty.try_begin_orbit(&alt_press([600.0, 400.0]), VP, &world),
"nothing selected"
);
assert!(empty.orbit.is_none());
let mut h = hook();
h.selection.set(vec!["box".to_string()]);
assert!(
!h.try_begin_orbit(&alt_press([600.0, hud::BAR_H - 1.0]), VP, &world),
"a press on the top bar is the bar's"
);
let mut playing = hook();
playing.selection.set(vec!["box".to_string()]);
playing.sim_toggle_play();
assert!(
!playing.try_begin_orbit(&alt_press([600.0, 400.0]), VP, &world),
"play mode owns the camera"
);
}
#[test]
fn a_camera_on_the_pivot_declines_the_tumble() {
let (world, _) = camera_world([0.0, 0.0, 0.0]);
let mut h = hook();
h.selection.set(vec!["box".to_string()]);
assert!(!h.try_begin_orbit(&alt_press([600.0, 400.0]), VP, &world));
}
#[test]
fn framing_the_selection_glides_the_camera_to_fit_it() {
let (mut world, _) = camera_world([0.0, 0.0, 100.0]);
let mut h = hook();
h.selection.set(vec!["box".to_string()]);
let input = FrameInput {
viewport: VP,
..Default::default()
};
h.frame_selection(input.viewport, &world);
assert!(h.glide.is_some(), "F armed a glide");
h.drive_glide(&input, &mut world);
assert!(h.glide.is_some(), "the glide is still in flight");
finish_glide(&mut h, &mut world);
assert!(h.glide.is_none(), "the glide finished and cleared itself");
let cam = camera(&world);
assert_eq!((cam.yaw, cam.pitch), (0.0, 0.0), "the look direction held");
let dist = (cam.position[2]).abs();
assert!(
dist < 100.0 && dist > 1.0,
"the camera closed on the box without landing inside it, at z={dist}"
);
}
#[test]
fn framing_nothing_starts_no_glide() {
let (world, _) = camera_world([0.0, 0.0, 10.0]);
let mut h = hook();
h.frame_selection(VP, &world);
assert!(h.glide.is_none());
}
#[test]
fn steering_during_a_glide_hands_the_camera_back() {
for steer in [
FrameInput {
forward: true,
..Default::default()
},
FrameInput {
backward: true,
..Default::default()
},
FrameInput {
left: true,
..Default::default()
},
FrameInput {
right: true,
..Default::default()
},
FrameInput {
mouse_dx: 3.0,
..Default::default()
},
FrameInput {
mouse_dy: -3.0,
..Default::default()
},
] {
let (mut world, _) = camera_world([0.0, 0.0, 100.0]);
let mut h = hook();
h.selection.set(vec!["box".to_string()]);
h.frame_selection(VP, &world);
assert!(h.glide.is_some());
h.drive_glide(&steer, &mut world);
assert!(h.glide.is_none(), "{steer:?} did not end the glide");
assert!(h.fly_clock.is_none());
}
}
#[test]
fn digit_keys_map_to_bookmark_slots() {
let digits = [
InputKey::Num1,
InputKey::Num2,
InputKey::Num3,
InputKey::Num4,
InputKey::Num5,
InputKey::Num6,
InputKey::Num7,
InputKey::Num8,
InputKey::Num9,
];
for (i, key) in digits.into_iter().enumerate() {
assert_eq!(bookmarks::slot_for(key), Some(i));
}
for key in [InputKey::A, InputKey::Num0, InputKey::Space] {
assert_eq!(bookmarks::slot_for(key), None);
}
}
#[test]
fn a_saved_bookmark_recalls_the_pose_it_captured() {
isolate_state_dir();
let _guard = crate::test_support::lock();
let (mut world, _) = camera_world([1.0, 2.0, 3.0]);
let mut h = hook();
h.save_bookmark(0, &world);
assert!(h.bookmarks[0].is_some(), "the slot captured a pose");
if let Some(cam) = world.query_mut::<Camera3D>().next() {
cam.position = [50.0, 50.0, 50.0];
}
h.recall_bookmark(0, &world);
assert!(h.glide.is_some(), "the recall armed a glide back");
finish_glide(&mut h, &mut world);
let p = camera(&world).position;
for axis in 0..3 {
assert!(
(p[axis] - [1.0, 2.0, 3.0][axis]).abs() < 0.01,
"the glide landed on the saved pose, got {p:?}"
);
}
}
#[test]
fn recalling_an_empty_slot_does_nothing() {
let (world, _) = camera_world([1.0, 2.0, 3.0]);
let mut h = hook();
h.recall_bookmark(4, &world);
assert!(h.glide.is_none());
}
#[test]
fn a_recall_cancels_an_in_flight_tumble() {
isolate_state_dir();
let _guard = crate::test_support::lock();
let (world, _) = camera_world([0.0, 0.0, 10.0]);
let mut h = hook();
h.selection.set(vec!["box".to_string()]);
h.save_bookmark(2, &world);
assert!(h.try_begin_orbit(&alt_press([600.0, 400.0]), VP, &world));
h.recall_bookmark(2, &world);
assert!(h.orbit.is_none(), "the tumble was dropped");
assert!(h.glide.is_some());
}
#[test]
fn selection_bounds_fall_back_to_a_billboards_transform() {
crate::ecs::asset_id::reset_interner();
let id = crate::ecs::asset_id::intern("lamp");
let mut world = World::new();
let entity = world.push(crate::components::Transform {
position: [4.0, 5.0, 6.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
});
let mut by_name = std::collections::BTreeMap::new();
by_name.insert(id, entity);
world.insert_resource(concinnity_core::ecs::EntityByName(by_name));
world.insert_resource(crate::ecs::PickIndex::default());
let mut h = hook();
h.selection.set(vec!["lamp".to_string()]);
let (mn, mx) = h
.selection_bounds(&world)
.expect("bounds from the transform");
assert!(
mn[0] < 4.0 && mx[0] > 4.0,
"padded around x, got {mn:?}..{mx:?}"
);
assert!(mn[1] < 5.0 && mx[1] > 5.0);
assert!(mn[2] < 6.0 && mx[2] > 6.0);
}