use crate::camera::MainCamera;
use crate::mesh::{BatchedMesh, TriangleEntityMapping};
use crate::storage::{save_selection, SelectionStorage};
use bevy::math::Affine3A;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use rustc_hash::FxHashSet;
pub struct PickingPlugin;
impl Plugin for PickingPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SelectionState>()
.init_resource::<PickingSettings>()
.add_systems(
Update,
(picking_system, hover_system)
.after(crate::camera::CameraPlugin::input_system_set()),
);
}
}
#[derive(Resource, Default)]
pub struct SelectionState {
pub selected: FxHashSet<u64>,
pub hovered: Option<u64>,
}
impl SelectionState {
pub fn is_selected(&self, id: u64) -> bool {
self.selected.contains(&id)
}
pub fn select(&mut self, id: u64) {
self.selected.clear();
self.selected.insert(id);
self.save();
}
pub fn toggle(&mut self, id: u64) {
if self.selected.contains(&id) {
self.selected.remove(&id);
} else {
self.selected.insert(id);
}
self.save();
}
pub fn add(&mut self, id: u64) {
self.selected.insert(id);
self.save();
}
pub fn remove(&mut self, id: u64) {
self.selected.remove(&id);
self.save();
}
pub fn clear(&mut self) {
self.selected.clear();
self.save();
}
fn save(&self) {
let storage = SelectionStorage {
selected_ids: self.selected.iter().copied().collect(),
hovered_id: self.hovered,
};
save_selection(&storage);
}
}
#[derive(Resource)]
pub struct PickingSettings {
pub enabled: bool,
pub hover_throttle: u32,
}
impl Default for PickingSettings {
fn default() -> Self {
Self {
enabled: true,
hover_throttle: 3, }
}
}
#[allow(clippy::too_many_arguments)]
fn picking_system(
keyboard: Res<ButtonInput<KeyCode>>,
cameras: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
batched_meshes: Query<(&BatchedMesh, &GlobalTransform, &Mesh3d)>,
triangle_mapping: Res<TriangleEntityMapping>,
meshes: Res<Assets<Mesh>>,
mut selection: ResMut<SelectionState>,
settings: Res<PickingSettings>,
mut camera_controller: ResMut<crate::camera::CameraController>,
) {
if !settings.enabled {
return;
}
if !camera_controller.just_clicked {
return;
}
camera_controller.just_clicked = false;
let Ok((camera, camera_transform)) = cameras.single() else {
return;
};
let click_pos = camera_controller.drag_start_pos;
let Ok(ray) = camera.viewport_to_world(camera_transform, click_pos) else {
return;
};
let mut closest: Option<(u64, f32)> = None;
for (batched_mesh, transform, mesh_handle) in batched_meshes.iter() {
if let Some(mesh) = meshes.get(&mesh_handle.0) {
if let Some((distance, triangle_index)) =
ray_mesh_intersection_with_triangle(&ray, mesh, transform)
{
if let Some(entity_id) =
triangle_mapping.get_entity(batched_mesh.is_transparent, triangle_index)
{
if closest.map(|(_, d)| distance < d).unwrap_or(true) {
closest = Some((entity_id, distance));
}
}
}
}
}
if let Some((entity_id, _)) = closest {
let ctrl_pressed = keyboard.pressed(KeyCode::ControlLeft)
|| keyboard.pressed(KeyCode::ControlRight)
|| keyboard.pressed(KeyCode::SuperLeft)
|| keyboard.pressed(KeyCode::SuperRight);
if ctrl_pressed {
selection.toggle(entity_id);
} else {
selection.select(entity_id);
}
} else {
if !keyboard.pressed(KeyCode::ControlLeft) && !keyboard.pressed(KeyCode::ControlRight) {
selection.clear();
}
}
}
#[allow(clippy::too_many_arguments)]
fn hover_system(
windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
batched_meshes: Query<(&BatchedMesh, &GlobalTransform, &Mesh3d)>,
triangle_mapping: Res<TriangleEntityMapping>,
meshes: Res<Assets<Mesh>>,
mut selection: ResMut<SelectionState>,
settings: Res<PickingSettings>,
mut frame_counter: Local<u32>,
) {
if !settings.enabled {
return;
}
*frame_counter += 1;
if !(*frame_counter).is_multiple_of(settings.hover_throttle) {
return;
}
let Ok(window) = windows.single() else { return };
let Some(cursor_pos) = window.cursor_position() else {
if selection.hovered.is_some() {
selection.hovered = None;
}
return;
};
let Ok((camera, camera_transform)) = cameras.single() else {
return;
};
let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_pos) else {
return;
};
let mut closest: Option<(u64, f32)> = None;
for (batched_mesh, transform, mesh_handle) in batched_meshes.iter() {
if let Some(mesh) = meshes.get(&mesh_handle.0) {
if let Some((distance, triangle_index)) =
ray_mesh_intersection_with_triangle(&ray, mesh, transform)
{
if let Some(entity_id) =
triangle_mapping.get_entity(batched_mesh.is_transparent, triangle_index)
{
if closest.map(|(_, d)| distance < d).unwrap_or(true) {
closest = Some((entity_id, distance));
}
}
}
}
}
let new_hovered = closest.map(|(id, _)| id);
if selection.hovered != new_hovered {
selection.hovered = new_hovered;
}
}
fn ray_mesh_intersection_with_triangle(
ray: &Ray3d,
mesh: &Mesh,
transform: &GlobalTransform,
) -> Option<(f32, usize)> {
let positions = mesh.attribute(Mesh::ATTRIBUTE_POSITION)?.as_float3()?;
let transform_matrix = transform.affine();
let (min, max) = compute_world_aabb(positions, &transform_matrix);
if !ray_aabb_intersects(ray, min, max) {
return None;
}
let indices = mesh.indices()?;
let indices: Vec<usize> = indices.iter().collect();
let mut closest: Option<(f32, usize)> = None;
for (tri_idx, chunk) in indices.chunks(3).enumerate() {
if chunk.len() < 3 {
continue;
}
let v0 = transform_matrix.transform_point3(Vec3::from(positions[chunk[0]]));
let v1 = transform_matrix.transform_point3(Vec3::from(positions[chunk[1]]));
let v2 = transform_matrix.transform_point3(Vec3::from(positions[chunk[2]]));
if let Some(t) = ray_triangle_intersection(ray, v0, v1, v2) {
if t > 0.0 && closest.map(|(d, _)| t < d).unwrap_or(true) {
closest = Some((t, tri_idx));
}
}
}
closest
}
fn compute_world_aabb(positions: &[[f32; 3]], transform: &Affine3A) -> (Vec3, Vec3) {
let mut min = Vec3::splat(f32::MAX);
let mut max = Vec3::splat(f32::MIN);
for pos in positions {
let world_pos = transform.transform_point3(Vec3::from(*pos));
min = min.min(world_pos);
max = max.max(world_pos);
}
(min, max)
}
fn ray_triangle_intersection(ray: &Ray3d, v0: Vec3, v1: Vec3, v2: Vec3) -> Option<f32> {
const EPSILON: f32 = 1e-7;
let edge1 = v1 - v0;
let edge2 = v2 - v0;
let h = ray.direction.cross(edge2);
let a = edge1.dot(h);
if a.abs() < EPSILON {
return None;
}
let f = 1.0 / a;
let s = ray.origin - v0;
let u = f * s.dot(h);
if !(0.0..=1.0).contains(&u) {
return None;
}
let q = s.cross(edge1);
let v = f * ray.direction.dot(q);
if v < 0.0 || u + v > 1.0 {
return None;
}
let t = f * edge2.dot(q);
if t > EPSILON {
Some(t)
} else {
None
}
}
fn ray_aabb_intersects(ray: &Ray3d, min: Vec3, max: Vec3) -> bool {
let inv_dir = Vec3::new(
1.0 / ray.direction.x,
1.0 / ray.direction.y,
1.0 / ray.direction.z,
);
let t1 = (min - ray.origin) * inv_dir;
let t2 = (max - ray.origin) * inv_dir;
let tmin = t1.min(t2);
let tmax = t1.max(t2);
let t_enter = tmin.x.max(tmin.y).max(tmin.z);
let t_exit = tmax.x.min(tmax.y).min(tmax.z);
t_enter <= t_exit && t_exit >= 0.0
}