bevy/crates/bevy_sprite/src/picking_backend.rs

162 lines
6.9 KiB
Rust
Raw Normal View History

//! 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.
Add `core` and `alloc` over `std` Lints (#15281) # Objective - Fixes #6370 - Closes #6581 ## Solution - Added the following lints to the workspace: - `std_instead_of_core` - `std_instead_of_alloc` - `alloc_instead_of_core` - Used `cargo +nightly fmt` with [item level use formatting](https://rust-lang.github.io/rustfmt/?version=v1.6.0&search=#Item%5C%3A) to split all `use` statements into single items. - Used `cargo clippy --workspace --all-targets --all-features --fix --allow-dirty` to _attempt_ to resolve the new linting issues, and intervened where the lint was unable to resolve the issue automatically (usually due to needing an `extern crate alloc;` statement in a crate root). - Manually removed certain uses of `std` where negative feature gating prevented `--all-features` from finding the offending uses. - Used `cargo +nightly fmt` with [crate level use formatting](https://rust-lang.github.io/rustfmt/?version=v1.6.0&search=#Crate%5C%3A) to re-merge all `use` statements matching Bevy's previous styling. - Manually fixed cases where the `fmt` tool could not re-merge `use` statements due to conditional compilation attributes. ## Testing - Ran CI locally ## Migration Guide The MSRV is now 1.81. Please update to this version or higher. ## Notes - This is a _massive_ change to try and push through, which is why I've outlined the semi-automatic steps I used to create this PR, in case this fails and someone else tries again in the future. - Making this change has no impact on user code, but does mean Bevy contributors will be warned to use `core` and `alloc` instead of `std` where possible. - This lint is a critical first step towards investigating `no_std` options for Bevy. --------- Co-authored-by: François Mockers <francois.mockers@vleue.com>
2024-09-27 00:59:59 +00:00
use core::cmp::Reverse;
use crate::{Sprite, TextureAtlas, TextureAtlasLayout};
use bevy_app::prelude::*;
use bevy_asset::prelude::*;
use bevy_ecs::prelude::*;
use bevy_math::{prelude::*, FloatExt, FloatOrd};
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>>,
sprite_query: Query<(
Entity,
&Sprite,
Option<&TextureAtlas>,
&Handle<Image>,
&GlobalTransform,
Option<&PickingBehavior>,
&ViewVisibility,
)>,
mut output: EventWriter<PointerHits>,
) {
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)));
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;
};
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position)
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(
|(entity, sprite, atlas, image, sprite_transform, picking_behavior, ..)| {
if blocked {
return None;
}
// Hit box in sprite coordinate system
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()
}),
};
let anchor = sprite.anchor.as_vec();
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
&& picking_behavior.map(|p| p.should_block_lower) != Some(false);
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));
}
}