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>()
.init_resource::<MeasurementState>()
.add_systems(
Update,
(
poll_active_tool,
measure_system.after(poll_active_tool),
picking_system.after(measure_system),
hover_system,
draw_measurements,
)
.after(crate::camera::CameraPlugin::input_system_set()),
);
}
}
#[derive(Resource, Default)]
pub struct MeasurementState {
pub measurements: Vec<(Vec3, Vec3)>,
pub pending: Option<Vec3>,
pub active: bool,
}
#[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, Vec3)> = 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, hit_point)) =
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, hit_point));
}
}
}
}
}
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, _hit)) =
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, Vec3)> {
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, Vec3)> = 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) {
let hit_point = ray.origin + *ray.direction * t;
closest = Some((t, tri_idx, hit_point));
}
}
}
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 poll_active_tool(mut measurement: ResMut<MeasurementState>, mut frame: Local<u32>) {
*frame += 1;
if !(*frame).is_multiple_of(10) {
return;
}
let is_measure = crate::storage::load_active_tool()
.map(|t| t == "measure")
.unwrap_or(false);
measurement.active = is_measure;
}
#[allow(clippy::too_many_arguments)]
fn measure_system(
cameras: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
batched_meshes: Query<(&BatchedMesh, &GlobalTransform, &Mesh3d)>,
meshes: Res<Assets<Mesh>>,
mut measurement: ResMut<MeasurementState>,
mut camera_controller: ResMut<crate::camera::CameraController>,
keyboard: Res<ButtonInput<KeyCode>>,
) {
if !measurement.active {
return;
}
if keyboard.just_pressed(KeyCode::Escape) {
measurement.pending = None;
measurement.measurements.clear();
crate::log_info("[Measure] Cleared all measurements");
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<(f32, Vec3)> = None;
for (_batched_mesh, transform, mesh_handle) in batched_meshes.iter() {
if let Some(mesh) = meshes.get(&mesh_handle.0) {
if let Some((distance, _tri, hit_point)) =
ray_mesh_intersection_with_triangle(&ray, mesh, transform)
{
if closest.map(|(d, _)| distance < d).unwrap_or(true) {
closest = Some((distance, hit_point));
}
}
}
}
if let Some((_, hit_point)) = closest {
crate::storage::save_measure_point(&crate::storage::MeasurePointStorage {
x: hit_point.x,
y: hit_point.y,
z: hit_point.z,
});
if let Some(start) = measurement.pending.take() {
let dist = (hit_point - start).length();
measurement.measurements.push((start, hit_point));
crate::log_info(&format!("[Measure] Distance: {:.3}m", dist));
} else {
measurement.pending = Some(hit_point);
crate::log_info(&format!(
"[Measure] Point 1 set ({:.2}, {:.2}, {:.2}) — click point 2",
hit_point.x, hit_point.y, hit_point.z,
));
}
}
}
fn draw_measurements(measurement: Res<MeasurementState>, mut gizmos: Gizmos) {
if !measurement.active && measurement.measurements.is_empty() && measurement.pending.is_none() {
return;
}
let yellow = Color::srgb(1.0, 0.85, 0.0);
let red = Color::srgb(1.0, 0.3, 0.3);
let cyan = Color::srgb(0.0, 0.9, 1.0);
for (start, end) in &measurement.measurements {
gizmos.line(*start, *end, yellow);
let dir = (*end - *start).normalize();
let offset = if dir.cross(Vec3::Y).length() > 0.1 {
dir.cross(Vec3::Y).normalize() * 0.02
} else {
dir.cross(Vec3::X).normalize() * 0.02
};
gizmos.line(*start + offset, *end + offset, yellow);
gizmos.line(*start - offset, *end - offset, yellow);
let sphere_size = 0.06;
gizmos.sphere(Isometry3d::from_translation(*start), sphere_size, red);
gizmos.sphere(Isometry3d::from_translation(*end), sphere_size, red);
let mid = (*start + *end) / 2.0;
gizmos.sphere(Isometry3d::from_translation(mid), 0.03, yellow);
}
if let Some(start) = measurement.pending {
let size = 0.12;
gizmos.sphere(Isometry3d::from_translation(start), 0.08, cyan);
gizmos.line(start - Vec3::X * size, start + Vec3::X * size, cyan);
gizmos.line(start - Vec3::Y * size, start + Vec3::Y * size, cyan);
gizmos.line(start - Vec3::Z * size, start + Vec3::Z * size, cyan);
}
}
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
}