use concinnity_core::components::Camera3D;
use concinnity_core::components::FrameInput;
use concinnity_core::ecs::PickIndex;
use concinnity_core::ecs::World;
use concinnity_core::ecs::asset_id::AssetId;
use concinnity_core::math::pick::{PickRay, ray_aabb, screen_ray};
use concinnity_host::thread::asset_id;
use super::EditorHook;
use crate::editor::panels::registry::PanelKey;
use crate::editor::viewport::highlight;
const CYCLE_SLOP_PX: f32 = 4.0;
pub(super) struct PickLast {
pos: [f32; 2],
hits: Vec<AssetId>,
index: usize,
}
pub(super) fn resolve_name(id: AssetId) -> Option<String> {
asset_id::name_of(id)
}
impl EditorHook {
pub(super) fn click_world(&mut self, input: &FrameInput, world: &mut World) {
let mouse = [input.mouse_x, input.mouse_y];
let Some(ray) = camera_ray(world, input.viewport, mouse) else {
return;
};
let hits = ray_hits(world, &ray, &self.locked_ids());
if hits.is_empty() {
self.begin_marquee(mouse, input.shift);
return;
}
if input.shift {
self.pick_last = None;
let Some(name) = resolve_name(hits[0]) else {
return;
};
if self.toggle_named(&name) {
self.select_in_viewport(&name, world);
} else {
self.follow_active(world);
}
return;
}
let index = match &self.pick_last {
Some(last)
if last.hits == hits
&& (last.pos[0] - mouse[0]).abs() <= CYCLE_SLOP_PX
&& (last.pos[1] - mouse[1]).abs() <= CYCLE_SLOP_PX =>
{
(last.index + 1) % hits.len()
}
_ => 0,
};
let picked = hits[index];
self.pick_last = Some(PickLast {
pos: mouse,
hits,
index,
});
let Some(name) = resolve_name(picked) else {
self.selection.clear();
return;
};
self.select_named(&name);
self.select_in_viewport(&name, world);
}
pub(super) fn select_in_viewport(&mut self, name: &str, world: &mut World) {
self.follow_active(world);
self.reveal_in_tree(name, world);
}
pub(super) fn focus_ui_on(&mut self, name: &str, world: &mut World) {
self.open[PanelKey::Assets] = true;
self.assets.picker_open = false;
self.assets.row_menu = None;
self.refresh_tree_if_needed();
self.open_asset_form(name, world);
self.reveal_in_tree(name, world);
}
pub(super) fn follow_active(&mut self, world: &mut World) {
if !self.form_open() {
return;
}
let Some(name) = self.selection.active().and_then(|h| self.handle_name(h)) else {
return;
};
self.open_asset_form(&name, world);
}
pub(super) fn drive_highlight(&self, world: &mut World, vp: [f32; 2], shown: bool) {
if !shown || self.sim.playing() {
highlight::hide(world);
return;
}
let active = self.selection.active();
let rects: Vec<([f32; 4], bool)> = self
.selection
.iter()
.filter_map(|handle| {
let name = self.handle_name(handle)?;
Self::member_rect(world, vp, &name).map(|r| (r, Some(handle) == active))
})
.take(highlight::MAX_RINGS)
.collect();
highlight::place_all(world, &rects);
}
pub(super) fn member_rect(world: &World, vp: [f32; 2], name: &str) -> Option<[f32; 4]> {
let id = asset_id::lookup(name)?;
let index = world.resource::<PickIndex>()?;
let entry = index.entries.iter().find(|e| e.asset_id == id)?;
let cam = world.query::<Camera3D>().next()?;
highlight::screen_rect(
&cam.view_matrix,
cam.fov_y_degrees.to_radians(),
vp,
entry.bb_min,
entry.bb_max,
)
}
}
pub(super) fn camera_ray(world: &World, viewport: [f32; 2], mouse: [f32; 2]) -> Option<PickRay> {
let cam = world.query::<Camera3D>().next()?;
screen_ray(
&cam.view_matrix,
cam.position,
cam.fov_y_degrees.to_radians(),
viewport,
mouse,
)
}
pub(super) fn ray_hits(
world: &World,
ray: &PickRay,
locked: &std::collections::BTreeSet<AssetId>,
) -> Vec<AssetId> {
let Some(index) = world.resource::<PickIndex>() else {
return Vec::new();
};
let mut hits: Vec<(f32, AssetId)> = index
.entries
.iter()
.filter(|e| !locked.contains(&e.asset_id))
.filter_map(|e| ray_aabb(ray, e.bb_min, e.bb_max).map(|t| (t, e.asset_id)))
.collect();
hits.sort_by(|a, b| a.0.total_cmp(&b.0));
hits.into_iter().map(|(_, id)| id).collect()
}