2024-08-26 18:01:32 +00:00
|
|
|
//! A [`bevy_picking`] backend for sprites. Works for simple sprites and sprite atlases. Works for
|
|
|
|
//! sprites with arbitrary transforms. Picking is done based on sprite bounds, not visible pixels.
|
|
|
|
//! This means a partially transparent sprite is pickable even in its transparent areas.
|
|
|
|
|
2024-09-27 00:59:59 +00:00
|
|
|
use core::cmp::Reverse;
|
2024-08-26 18:01:32 +00:00
|
|
|
|
|
|
|
use crate::{Sprite, TextureAtlas, TextureAtlasLayout};
|
|
|
|
use bevy_app::prelude::*;
|
|
|
|
use bevy_asset::prelude::*;
|
|
|
|
use bevy_ecs::prelude::*;
|
2024-09-17 23:27:53 +00:00
|
|
|
use bevy_math::{prelude::*, FloatExt, FloatOrd};
|
2024-08-26 18:01:32 +00:00
|
|
|
use bevy_picking::backend::prelude::*;
|
|
|
|
use bevy_render::prelude::*;
|
|
|
|
use bevy_transform::prelude::*;
|
|
|
|
use bevy_window::PrimaryWindow;
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct SpritePickingBackend;
|
|
|
|
|
|
|
|
impl Plugin for SpritePickingBackend {
|
|
|
|
fn build(&self, app: &mut App) {
|
|
|
|
app.add_systems(PreUpdate, sprite_picking.in_set(PickSet::Backend));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn sprite_picking(
|
|
|
|
pointers: Query<(&PointerId, &PointerLocation)>,
|
|
|
|
cameras: Query<(Entity, &Camera, &GlobalTransform, &OrthographicProjection)>,
|
|
|
|
primary_window: Query<Entity, With<PrimaryWindow>>,
|
|
|
|
images: Res<Assets<Image>>,
|
|
|
|
texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,
|
2024-09-30 17:03:31 +00:00
|
|
|
sprite_query: Query<(
|
|
|
|
Entity,
|
|
|
|
&Sprite,
|
|
|
|
Option<&TextureAtlas>,
|
|
|
|
&Handle<Image>,
|
|
|
|
&GlobalTransform,
|
2024-10-07 17:09:57 +00:00
|
|
|
Option<&PickingBehavior>,
|
2024-09-30 17:03:31 +00:00
|
|
|
&ViewVisibility,
|
|
|
|
)>,
|
2024-08-26 18:01:32 +00:00
|
|
|
mut output: EventWriter<PointerHits>,
|
|
|
|
) {
|
2024-09-17 23:27:53 +00:00
|
|
|
let mut sorted_sprites: Vec<_> = sprite_query
|
|
|
|
.iter()
|
|
|
|
.filter(|x| !x.4.affine().is_nan())
|
|
|
|
.collect();
|
|
|
|
sorted_sprites.sort_by_key(|x| Reverse(FloatOrd(x.4.translation().z)));
|
2024-08-26 18:01:32 +00:00
|
|
|
|
|
|
|
let primary_window = primary_window.get_single().ok();
|
|
|
|
|
|
|
|
for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| {
|
|
|
|
pointer_location.location().map(|loc| (pointer, loc))
|
|
|
|
}) {
|
|
|
|
let mut blocked = false;
|
|
|
|
let Some((cam_entity, camera, cam_transform, cam_ortho)) = cameras
|
|
|
|
.iter()
|
|
|
|
.filter(|(_, camera, _, _)| camera.is_active)
|
|
|
|
.find(|(_, camera, _, _)| {
|
|
|
|
camera
|
|
|
|
.target
|
|
|
|
.normalize(primary_window)
|
|
|
|
.map(|x| x == location.target)
|
|
|
|
.unwrap_or(false)
|
|
|
|
})
|
|
|
|
else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
2024-09-03 19:45:15 +00:00
|
|
|
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position)
|
2024-08-26 18:01:32 +00:00
|
|
|
else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
let cursor_ray_len = cam_ortho.far - cam_ortho.near;
|
|
|
|
let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len;
|
|
|
|
|
|
|
|
let picks: Vec<(Entity, HitData)> = sorted_sprites
|
|
|
|
.iter()
|
|
|
|
.copied()
|
|
|
|
.filter(|(.., visibility)| visibility.get())
|
|
|
|
.filter_map(
|
2024-10-07 17:09:57 +00:00
|
|
|
|(entity, sprite, atlas, image, sprite_transform, picking_behavior, ..)| {
|
2024-08-26 18:01:32 +00:00
|
|
|
if blocked {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hit box in sprite coordinate system
|
2024-09-30 17:03:31 +00:00
|
|
|
let extents = match (sprite.custom_size, atlas) {
|
|
|
|
(Some(custom_size), _) => custom_size,
|
|
|
|
(None, None) => images.get(image)?.size().as_vec2(),
|
|
|
|
(None, Some(atlas)) => texture_atlas_layout
|
|
|
|
.get(&atlas.layout)
|
|
|
|
.and_then(|layout| layout.textures.get(atlas.index))
|
|
|
|
// Dropped atlas layouts and indexes out of bounds are rendered as a sprite
|
|
|
|
.map_or(images.get(image)?.size().as_vec2(), |rect| {
|
|
|
|
rect.size().as_vec2()
|
|
|
|
}),
|
2024-08-26 18:01:32 +00:00
|
|
|
};
|
2024-09-30 17:03:31 +00:00
|
|
|
let anchor = sprite.anchor.as_vec();
|
2024-08-26 18:01:32 +00:00
|
|
|
let center = -anchor * extents;
|
|
|
|
let rect = Rect::from_center_half_size(center, extents / 2.0);
|
|
|
|
|
|
|
|
// Transform cursor line segment to sprite coordinate system
|
|
|
|
let world_to_sprite = sprite_transform.affine().inverse();
|
|
|
|
let cursor_start_sprite =
|
|
|
|
world_to_sprite.transform_point3(cursor_ray_world.origin);
|
|
|
|
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
|
|
|
|
|
|
|
|
// Find where the cursor segment intersects the plane Z=0 (which is the sprite's
|
|
|
|
// plane in sprite-local space). It may not intersect if, for example, we're
|
|
|
|
// viewing the sprite side-on
|
|
|
|
if cursor_start_sprite.z == cursor_end_sprite.z {
|
|
|
|
// Cursor ray is parallel to the sprite and misses it
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
let lerp_factor =
|
|
|
|
f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
|
|
|
|
if !(0.0..=1.0).contains(&lerp_factor) {
|
|
|
|
// Lerp factor is out of range, meaning that while an infinite line cast by
|
|
|
|
// the cursor would intersect the sprite, the sprite is not between the
|
|
|
|
// camera's near and far planes
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
// Otherwise we can interpolate the xy of the start and end positions by the
|
|
|
|
// lerp factor to get the cursor position in sprite space!
|
|
|
|
let cursor_pos_sprite = cursor_start_sprite
|
|
|
|
.lerp(cursor_end_sprite, lerp_factor)
|
|
|
|
.xy();
|
|
|
|
|
|
|
|
let is_cursor_in_sprite = rect.contains(cursor_pos_sprite);
|
|
|
|
|
|
|
|
blocked = is_cursor_in_sprite
|
2024-10-07 17:09:57 +00:00
|
|
|
&& picking_behavior.map(|p| p.should_block_lower) != Some(false);
|
2024-08-26 18:01:32 +00:00
|
|
|
|
|
|
|
is_cursor_in_sprite.then(|| {
|
|
|
|
let hit_pos_world =
|
|
|
|
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
|
|
|
|
// Transform point from world to camera space to get the Z distance
|
|
|
|
let hit_pos_cam = cam_transform
|
|
|
|
.affine()
|
|
|
|
.inverse()
|
|
|
|
.transform_point3(hit_pos_world);
|
|
|
|
// HitData requires a depth as calculated from the camera's near clipping plane
|
|
|
|
let depth = -cam_ortho.near - hit_pos_cam.z;
|
|
|
|
(
|
|
|
|
entity,
|
|
|
|
HitData::new(
|
|
|
|
cam_entity,
|
|
|
|
depth,
|
|
|
|
Some(hit_pos_world),
|
|
|
|
Some(*sprite_transform.back()),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let order = camera.order as f32;
|
|
|
|
output.send(PointerHits::new(*pointer, picks, order));
|
|
|
|
}
|
|
|
|
}
|