use concinnity_cook::authoring::registry::RegisteredType;
use concinnity_core::components::FrameInput;
use concinnity_core::components::Sprite;
use concinnity_core::ecs::World;
use concinnity_host::thread::asset_id;
use std::collections::BTreeSet;
use super::fixtures::{click_at, entry, hook, pick_world, release_at, set_input};
use crate::editor::hook::FormTarget;
use crate::editor::hook::pick::{camera_ray, ray_hits};
use crate::frame_hook::FrameHook;
use crate::editor::panels::asset_tree::{self, TreeGroup};
use crate::editor::panels::assets_panel;
use crate::editor::hook::tests::fixtures::entry_target;
use crate::editor::hook::tests::fixtures::{active, selected};
use crate::editor::panels::registry::PanelKey;
use crate::editor::sim;
use crate::editor::viewport::highlight;
use crate::test_support::isolate_state_dir;
#[test]
fn viewport_click_picks_the_nearest_prop_without_opening_a_form() {
asset_id::reset_interner();
let near = asset_id::intern("box_near");
let far = asset_id::intern("box_far");
let mut world = pick_world(
[0.0; 3],
vec![
(far, [-1.0, -1.0, -11.0], [1.0, 1.0, -9.0]),
(near, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0]),
],
);
let mut h = hook(vec![
entry("box_near", "Sprite"),
entry("box_far", "Sprite"),
]);
click_at(&mut world, &mut h, [640.0, 360.0]);
assert_eq!(active(&h).as_deref(), Some("box_near"), "nearest hit wins");
assert!(!h.open[PanelKey::Assets], "a viewport click opens no panel");
assert!(!h.form_open(), "a viewport click opens no form");
h.open_asset_form("box_far", &mut world);
assert_eq!(h.form.target, entry_target(&h, 1));
click_at(&mut world, &mut h, [650.0, 370.0]);
assert_eq!(
h.form.target,
entry_target(&h, 0),
"the open form retargets to the picked entry"
);
assert_eq!(h.form.selected_type.as_deref(), Some("Sprite"));
}
#[test]
fn repeat_viewport_clicks_cycle_and_empty_space_clears() {
asset_id::reset_interner();
let near = asset_id::intern("box_near");
let far = asset_id::intern("box_far");
let mut world = pick_world(
[0.0; 3],
vec![
(near, [-7.1, -4.3, -6.0], [-5.1, -2.3, -4.0]),
(far, [-13.2, -7.7, -11.0], [-11.2, -5.7, -9.0]),
],
);
let mut h = hook(vec![
entry("box_near", "Sprite"),
entry("box_far", "Sprite"),
]);
click_at(&mut world, &mut h, [200.0, 600.0]);
assert_eq!(active(&h).as_deref(), Some("box_near"));
click_at(&mut world, &mut h, [201.0, 601.0]);
assert_eq!(
active(&h).as_deref(),
Some("box_far"),
"a repeat click reaches the occluded box"
);
assert_eq!(
selected(&h).len(),
1,
"a plain click replaces, never accumulates"
);
click_at(&mut world, &mut h, [200.0, 600.0]);
assert_eq!(
active(&h).as_deref(),
Some("box_near"),
"the cycle wraps back to the front"
);
click_at(&mut world, &mut h, [250.0, 450.0]);
assert_eq!(
active(&h).as_deref(),
Some("box_near"),
"the selection survives until the release decides click vs marquee"
);
release_at(&mut world, &mut h, [250.0, 450.0]);
assert_eq!(
active(&h).as_deref(),
None,
"empty space clears the selection"
);
}
#[test]
fn viewport_click_on_an_unknown_asset_selects_without_a_form() {
let _guard = crate::test_support::lock();
isolate_state_dir();
asset_id::reset_interner();
let generated = asset_id::intern("some_generated_asset");
let mut world = pick_world(
[0.0; 3],
vec![(generated, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0])],
);
let mut h = hook(vec![entry("box_near", "Sprite")]);
click_at(&mut world, &mut h, [640.0, 360.0]);
assert_eq!(active(&h).as_deref(), Some("some_generated_asset"));
assert!(
!h.open[PanelKey::Assets],
"no panel opens on a viewport click"
);
assert_eq!(h.form.target, FormTarget::New);
assert!(!h.form_open());
}
#[test]
fn a_history_jump_keeps_a_selection_its_entries_survive() {
asset_id::reset_interner();
let id = asset_id::intern("box_near");
let mut world = pick_world([0.0; 3], vec![(id, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0])]);
let mut h = hook(vec![entry("box_near", "Sprite")]);
click_at(&mut world, &mut h, [640.0, 360.0]);
assert_eq!(active(&h).as_deref(), Some("box_near"));
h.entries.push(entry("b", "Sprite"));
h.mark_changed();
h.undo(&mut world);
assert_eq!(
active(&h).as_deref(),
Some("box_near"),
"undoing an unrelated add leaves the selected entry standing"
);
assert!(h.pick_last.is_none(), "the repeat-click cycle is dropped");
}
#[test]
fn a_history_jump_drops_a_member_whose_entry_it_removes() {
asset_id::reset_interner();
let id = asset_id::intern("added");
let mut world = pick_world([0.0; 3], vec![(id, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0])]);
let mut h = hook(vec![entry("box_near", "Sprite")]);
h.entries.push(entry("added", "Sprite"));
h.mark_changed();
click_at(&mut world, &mut h, [640.0, 360.0]);
assert_eq!(active(&h).as_deref(), Some("added"));
h.undo(&mut world);
assert_eq!(
active(&h).as_deref(),
None,
"the entry the selection addressed is gone"
);
}
#[test]
fn selection_ring_tracks_the_picked_asset() {
asset_id::reset_interner();
let id = asset_id::intern("box_near");
let mut world = pick_world([0.0; 3], vec![(id, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0])]);
let mut h = hook(vec![entry("box_near", "Sprite")]);
click_at(&mut world, &mut h, [640.0, 360.0]);
let ring = |world: &World| {
world
.get_by_id::<Sprite>(highlight::ids().sprites[0])
.cloned()
.expect("outline sprite injected")
};
let s = ring(&world);
assert!(s.visible, "the ring shows on pick");
let (cx, cy) = (s.x + s.width * 0.5, s.y + s.height * 0.5);
assert!(
(cx - 640.0).abs() < 2.0 && (cy - 360.0).abs() < 2.0,
"ring centered on the box, got ({cx}, {cy})"
);
assert!(s.border_width > 0.0 && s.tint[3] == 0.0, "border-only ring");
h.sim.state = sim::SimState::Playing;
set_input(
&mut world,
FrameInput {
viewport: [1280.0, 720.0],
..Default::default()
},
);
h.tick(&mut world);
assert!(!ring(&world).visible, "hidden in play mode");
h.sim.state = sim::SimState::Stopped;
h.tick(&mut world);
assert!(ring(&world).visible, "back in edit mode it returns");
}
#[test]
fn locked_assets_are_skipped_by_viewport_picking() {
asset_id::reset_interner();
let near = asset_id::intern("box_near");
let mut world = pick_world([0.0; 3], vec![(near, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0])]);
let mut h = hook(vec![entry("box_near", "Sprite")]);
h.locked_assets.insert(h.handle_for("box_near"));
click_at(&mut world, &mut h, [640.0, 360.0]);
assert_eq!(active(&h).as_deref(), None, "the locked box is not picked");
assert!(h.marquee.is_some(), "the click fell through to empty space");
}
const PICK_VIEWPORT: [f32; 2] = [1280.0, 720.0];
const CENTER_PIXEL: [f32; 2] = [640.0, 360.0];
#[test]
fn camera_ray_needs_a_camera() {
assert!(camera_ray(&World::new(), PICK_VIEWPORT, CENTER_PIXEL).is_none());
}
#[test]
fn camera_ray_through_the_center_follows_the_view_axis() {
let world = pick_world([0.0; 3], vec![]);
let ray = camera_ray(&world, PICK_VIEWPORT, CENTER_PIXEL).expect("the world has a camera");
assert_eq!(ray.origin, [0.0; 3]);
let [x, y, z] = ray.dir;
assert!(
x.abs() < 1e-2 && y.abs() < 1e-2 && (z + 1.0).abs() < 1e-2,
"got {:?}",
ray.dir
);
}
#[test]
fn ray_hits_orders_near_before_far_regardless_of_index_order() {
asset_id::reset_interner();
let near = asset_id::intern("box_near");
let far = asset_id::intern("box_far");
let world = pick_world(
[0.0; 3],
vec![
(far, [-1.0, -1.0, -11.0], [1.0, 1.0, -9.0]),
(near, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0]),
],
);
let ray = camera_ray(&world, PICK_VIEWPORT, CENTER_PIXEL).unwrap();
assert_eq!(ray_hits(&world, &ray, &BTreeSet::new()), vec![near, far]);
}
#[test]
fn ray_hits_skips_locked_ids() {
asset_id::reset_interner();
let near = asset_id::intern("box_near");
let far = asset_id::intern("box_far");
let world = pick_world(
[0.0; 3],
vec![
(near, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0]),
(far, [-1.0, -1.0, -11.0], [1.0, 1.0, -9.0]),
],
);
let ray = camera_ray(&world, PICK_VIEWPORT, CENTER_PIXEL).unwrap();
let locked = BTreeSet::from([near]);
assert_eq!(ray_hits(&world, &ray, &locked), vec![far]);
}
#[test]
fn ray_hits_is_empty_without_a_pick_index() {
let ray = camera_ray(&pick_world([0.0; 3], vec![]), PICK_VIEWPORT, CENTER_PIXEL).unwrap();
assert!(ray_hits(&World::new(), &ray, &BTreeSet::new()).is_empty());
}
#[test]
fn ray_hits_leaves_out_a_box_the_ray_misses() {
asset_id::reset_interner();
let near = asset_id::intern("box_near");
let aside = asset_id::intern("box_aside");
let world = pick_world(
[0.0; 3],
vec![
(aside, [5.0, -1.0, -6.0], [7.0, 1.0, -4.0]),
(near, [-1.0, -1.0, -6.0], [1.0, 1.0, -4.0]),
],
);
let ray = camera_ray(&world, PICK_VIEWPORT, CENTER_PIXEL).unwrap();
assert_eq!(ray_hits(&world, &ray, &BTreeSet::new()), vec![near]);
}
#[test]
fn viewport_pick_reveals_the_tree_row() {
let world = World::new();
let mut h = hook(Vec::new());
h.open[PanelKey::Assets] = true;
h.assets.stale = false;
h.assets.groups = vec![TreeGroup {
label: asset_tree::WORLD_GROUP.to_string(),
assets: (0..30)
.map(|i| asset_tree::TreeAsset {
name: format!("a{i:02}"),
asset_type: RegisteredType::Sprite,
badge: asset_tree::Badge::Authored,
promote: None,
})
.collect(),
}];
h.reveal_in_tree("a25", &world);
assert_eq!(h.assets.unfolded, vec![0], "the group unfolds");
assert_eq!(h.assets.scroll, 31 - assets_panel::ROW_POOL);
h.reveal_in_tree("a20", &world);
assert_eq!(h.assets.scroll, 31 - assets_panel::ROW_POOL);
}