use bevy::{
app::{Events, ManualEventReader},
prelude::*,
render::camera::Camera,
window::WindowId,
};
use std::collections::HashMap;
pub mod drag;
pub struct InteractionPlugin;
impl Plugin for InteractionPlugin {
fn build(&self, app: &mut AppBuilder) {
app
.init_resource::<InteractionState>()
.add_system_to_stage(CoreStage::PostUpdate, interaction_state_system.system())
.add_system_to_stage(CoreStage::PostUpdate, interaction_system.system());
}
}
pub struct InteractionDebugPlugin;
impl Plugin for InteractionDebugPlugin {
fn build(&self, app: &mut AppBuilder) {
app
.add_plugin(InteractionPlugin)
.add_plugin(bevy_prototype_lyon::prelude::ShapePlugin)
.add_system_to_stage(CoreStage::PreUpdate, setup_interaction_debug.system())
.add_system_to_stage(CoreStage::PostUpdate, cleanup_interaction_debug.system());
}
}
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, Default)]
pub struct Group(pub u8);
#[derive(Default)]
pub struct InteractionState {
pub ordered_interact_list_map: HashMap<Group, Vec<(Entity, Vec2)>>,
pub cursor_positions: HashMap<Group, Vec2>,
pub last_window_id: WindowId,
pub last_cursor_position: Vec2,
}
impl InteractionState {
pub fn get_group(&self, group: Group) -> Vec<(Entity, Vec2)> {
match self.ordered_interact_list_map.get(&group) {
Some(interactions) => interactions.clone(),
None => vec![],
}
}
}
pub struct InteractionSource {
pub groups: Vec<Group>,
pub cursor_events: ManualEventReader<CursorMoved>,
}
impl Default for InteractionSource {
fn default() -> Self {
Self {
groups: vec![Group::default()],
cursor_events: ManualEventReader::default(),
}
}
}
fn interaction_state_system(
mut interaction_state: ResMut<InteractionState>,
cursor_moved: Res<Events<CursorMoved>>,
windows: Res<Windows>,
mut sources: Query<(&mut InteractionSource, &GlobalTransform, Option<&Camera>)>,
) {
interaction_state.cursor_positions.clear();
for (mut interact_source, global_transform, camera) in sources.iter_mut() {
if let Some(evt) = interact_source.cursor_events.iter(&cursor_moved).last() {
interaction_state.last_window_id = evt.id;
interaction_state.last_cursor_position = evt.position;
}
let projection_matrix = match camera {
Some(camera) => camera.projection_matrix,
None => panic!("Interacting without camera not supported."),
};
if let Some(window) = windows.get(interaction_state.last_window_id) {
let screen_size = Vec2::from([window.width() as f32, window.height() as f32]);
let cursor_position = interaction_state.last_cursor_position;
let cursor_position_ndc = (cursor_position / screen_size) * 2.0 - Vec2::from([1.0, 1.0]);
let camera_matrix = global_transform.compute_matrix();
let ndc_to_world: Mat4 = camera_matrix * projection_matrix.inverse();
let cursor_position = ndc_to_world
.transform_point3(cursor_position_ndc.extend(1.0))
.truncate();
for group in &interact_source.groups {
if interaction_state
.cursor_positions
.insert(*group, cursor_position)
.is_some()
{
panic!(
"Multiple interaction sources have been added to interaction group {:?}",
group
);
}
}
}
}
}
pub struct Interactable {
pub groups: Vec<Group>,
pub bounding_box: (Vec2, Vec2),
}
impl Default for Interactable {
fn default() -> Self {
Self {
groups: vec![Group::default()],
bounding_box: (Vec2::default(), Vec2::default()),
}
}
}
fn interaction_system(
mut interaction_state: ResMut<InteractionState>,
interactables: Query<(Entity, &GlobalTransform, &Interactable)>,
) {
interaction_state.ordered_interact_list_map.clear();
for (entity, global_transform, interactable) in interactables.iter() {
let cursor_positions = interaction_state.cursor_positions.clone();
for (group, cursor_position) in cursor_positions {
if !interactable.groups.contains(&group) {
continue;
}
let relative_cursor_position = (cursor_position - global_transform.translation.truncate())
/ global_transform.scale.truncate();
if (interactable.bounding_box.0.x..interactable.bounding_box.1.x)
.contains(&relative_cursor_position.x)
&& (interactable.bounding_box.0.y..interactable.bounding_box.1.y)
.contains(&relative_cursor_position.y)
{
let interaction = (entity, cursor_position);
if let Some(list) = interaction_state.ordered_interact_list_map.get_mut(&group) {
list.push(interaction)
} else {
interaction_state
.ordered_interact_list_map
.insert(group, vec![interaction]);
}
}
}
}
}
pub struct DebugInteractable {
pub child: Entity,
}
fn setup_interaction_debug(
mut commands: Commands,
interactables: Query<(Entity, &Interactable), Added<Interactable>>,
) {
use bevy_prototype_lyon::prelude::*;
for (entity, interactable) in interactables.iter() {
let group_sum = interactable.groups.iter().fold(0, |acc, Group(n)| acc + n);
let (red, green) = match group_sum {
0 => (0, 0),
1 => (255, 0),
2 => (0, 255),
_ => (0, 0),
};
let blue = match interactable.groups.is_empty() {
true => 0,
false => 255,
};
let bounding_mesh = shapes::Polygon {
points: vec![
Vec2::new(interactable.bounding_box.0.x, interactable.bounding_box.0.y),
Vec2::new(interactable.bounding_box.1.x, interactable.bounding_box.0.y),
Vec2::new(interactable.bounding_box.1.x, interactable.bounding_box.1.y),
Vec2::new(interactable.bounding_box.0.x, interactable.bounding_box.1.y),
],
closed: true,
};
let child = commands
.spawn_bundle(GeometryBuilder::build_as(
&bounding_mesh,
ShapeColors::new(Color::rgb_u8(red, green, blue)),
DrawMode::Stroke(StrokeOptions::default()),
Transform::default(),
))
.id();
commands
.entity(entity)
.push_children(&vec![child])
.insert(DebugInteractable { child });
}
}
pub fn cleanup_interaction_debug(
mut commands: Commands,
removed_interactables: RemovedComponents<Interactable>,
interactables: Query<(Entity, &DebugInteractable)>,
) {
for entity in removed_interactables.iter() {
if let Ok(debug_interactable) = interactables.get_component::<DebugInteractable>(entity) {
commands.entity(debug_interactable.child).despawn();
} else {
warn!("Could not remove interactable debug from entity. Was already despawned?");
}
}
}