use crate::{
EguiContext, helpers,
input::{EguiContextPointerPosition, HoveredNonWindowEguiContext},
};
use bevy_asset::Assets;
use bevy_camera::{Camera, NormalizedRenderTarget, RenderTarget};
use bevy_ecs::{
change_detection::Res,
component::Component,
entity::Entity,
error::Result,
observer::On,
prelude::{AnyOf, Commands, Query, With},
};
use bevy_math::{Ray3d, Vec2};
use bevy_mesh::{Indices, Mesh, Mesh2d, Mesh3d, VertexAttributeValues};
use bevy_picking::{
Pickable,
events::{Move, Out, Over, Pointer},
mesh_picking::ray_cast::RayMeshHit,
prelude::{MeshRayCast, MeshRayCastSettings, RayCastVisibility},
};
use bevy_transform::components::GlobalTransform;
use bevy_window::PrimaryWindow;
use wgpu_types::PrimitiveTopology;
#[derive(Component)]
#[require(Pickable)]
pub struct PickableEguiContext(pub Entity);
pub fn handle_move_system(
event: On<Pointer<Move>>,
mut mesh_ray_cast: MeshRayCast,
mut egui_pointers: Query<&mut EguiContextPointerPosition>,
egui_contexts: Query<(&Camera, &GlobalTransform, &RenderTarget), With<EguiContext>>,
pickable_egui_context_query: Query<(&PickableEguiContext, AnyOf<(&Mesh2d, &Mesh3d)>)>,
primary_window_query: Query<Entity, With<PrimaryWindow>>,
meshes: Res<Assets<Mesh>>,
) -> Result {
let NormalizedRenderTarget::Window(_) = event.pointer_location.target else {
return Ok(());
};
let Ok((context_camera, global_transform, render_target)) = egui_contexts.get(event.hit.camera)
else {
return Ok(());
};
let settings = MeshRayCastSettings {
visibility: RayCastVisibility::Any,
filter: &|entity| pickable_egui_context_query.contains(entity),
early_exit_test: &|_| true,
};
let Some(ray) = make_ray(
&primary_window_query,
context_camera,
global_transform,
render_target,
&bevy_picking::pointer::PointerLocation {
location: Some(event.pointer_location.clone()),
},
) else {
return Ok(());
};
let &[
(
hit_entity,
RayMeshHit {
triangle_index: Some(triangle_index),
barycentric_coords,
..
},
),
] = mesh_ray_cast.cast_ray(ray, &settings)
else {
return Ok(());
};
let (&PickableEguiContext(context), mesh) = pickable_egui_context_query.get(hit_entity)?;
let (egui_mesh_camera, _, _) = egui_contexts.get(context)?;
let handle = match mesh {
(Some(handle), None) => handle.0.clone(),
(None, Some(handle)) => handle.0.clone(),
_ => unreachable!(),
};
let Some(mesh) = meshes.get(handle.id()) else {
return Ok(());
};
if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
panic!(
"Unexpected primitive topology for a picked mesh ({:?}): {:?}",
event.observer(),
mesh.primitive_topology()
);
}
let Some(indices) = mesh.indices() else {
return Ok(());
};
let Some(uv_values) =
mesh.attribute(Mesh::ATTRIBUTE_UV_0)
.and_then(|values| match (values, indices) {
(VertexAttributeValues::Float32x2(uvs), Indices::U16(indices)) => {
uv_values_for_triangle(indices, triangle_index, uvs)
}
(VertexAttributeValues::Float32x2(uvs), Indices::U32(indices)) => {
uv_values_for_triangle(indices, triangle_index, uvs)
}
_ => None,
})
else {
return Ok(());
};
let uv = Vec2::from_array(uv_values[0]) * barycentric_coords.x
+ Vec2::from_array(uv_values[1]) * barycentric_coords.y
+ Vec2::from_array(uv_values[2]) * barycentric_coords.z;
let Some(viewport_size) = egui_mesh_camera.logical_target_size() else {
return Ok(());
};
egui_pointers.get_mut(context)?.position = helpers::vec2_into_egui_pos2(viewport_size * uv);
Ok(())
}
pub fn handle_over_system(
event: On<Pointer<Over>>,
pickable_egui_context_query: Query<&PickableEguiContext>,
mut commands: Commands,
) {
if let Ok(&PickableEguiContext(context)) = pickable_egui_context_query.get(event.observer()) {
commands.insert_resource(HoveredNonWindowEguiContext(context));
}
}
pub fn handle_out_system(
event: On<Pointer<Out>>,
pickable_egui_context_query: Query<&PickableEguiContext>,
mut commands: Commands,
hovered_non_window_egui_context: Option<Res<HoveredNonWindowEguiContext>>,
) {
if let Ok(&PickableEguiContext(context)) = pickable_egui_context_query.get(event.observer())
&& hovered_non_window_egui_context
.as_deref()
.is_some_and(|&HoveredNonWindowEguiContext(hovered_context)| hovered_context == context)
{
commands.remove_resource::<HoveredNonWindowEguiContext>();
}
}
fn uv_values_for_triangle<I: TryInto<usize> + Clone + Copy>(
indices: &[I],
triangle_index: usize,
values: &[[f32; 2]],
) -> Option<[[f32; 2]; 3]> {
if !indices.len().is_multiple_of(3) || triangle_index >= indices.len() {
return None;
}
let i0 = indices[triangle_index * 3].try_into().ok()?;
let i1 = indices[triangle_index * 3 + 1].try_into().ok()?;
let i2 = indices[triangle_index * 3 + 2].try_into().ok()?;
Some([*values.get(i1)?, *values.get(i2)?, *values.get(i0)?])
}
fn make_ray(
primary_window_entity: &Query<Entity, With<PrimaryWindow>>,
camera: &Camera,
camera_tfm: &GlobalTransform,
render_target: &RenderTarget,
pointer_loc: &bevy_picking::pointer::PointerLocation,
) -> Option<Ray3d> {
let pointer_loc = pointer_loc.location()?;
if !pointer_loc.is_in_viewport(camera, render_target, primary_window_entity) {
return None;
}
let mut viewport_pos = pointer_loc.position;
if let Some(viewport) = &camera.viewport {
let viewport_logical = camera.to_logical(viewport.physical_position)?;
viewport_pos -= viewport_logical;
}
camera.viewport_to_world(camera_tfm, viewport_pos).ok()
}