use bevy::{
math::{IVec2, Mat4, UVec2, Vec2, Vec3},
prelude::{
App, Assets, Camera, Changed, Component, Entity, GlobalTransform, Image, Or, Plugin, Query,
Res,
},
render::camera::RenderTarget,
window::Windows,
};
use sark_grids::GridPoint;
use crate::{
renderer::{TerminalLayout, TileScaling},
Terminal,
};
pub(crate) struct ToWorldPlugin;
impl Plugin for ToWorldPlugin {
fn build(&self, app: &mut App) {
app.add_system(update_from_terminal)
.add_system(update_from_camera);
}
}
#[derive(Default, Component)]
pub struct ToWorld {
term_size: UVec2,
term_pos: Vec3,
layout: TerminalLayout,
camera_entity: Option<Entity>,
ndc_to_world: Mat4,
camera_pos: Vec3,
viewport_pos: Vec2,
viewport_size: Option<Vec2>,
}
impl ToWorld {
pub fn tile_to_world(&self, tile: impl GridPoint) -> Vec3 {
let term_pos = self.term_pos.truncate();
let term_offset = self.term_size.as_vec2() * Vec2::from(self.layout.pivot);
(tile.as_vec2() + term_pos - term_offset).extend(self.term_pos.z)
}
pub fn tile_center_to_world(&self, tile: impl GridPoint) -> Vec3 {
let center_offset = (self.world_unit() / 2.0).extend(0.0);
self.tile_to_world(tile) + center_offset
}
pub fn world_to_tile(&self, world: Vec2) -> IVec2 {
let term_pos = self.term_pos.truncate();
let term_offset = self.term_size.as_vec2() * Vec2::from(self.layout.pivot);
let xy = world - term_pos + term_offset;
xy.floor().as_ivec2()
}
pub fn world_unit(&self) -> Vec2 {
match self.layout.scaling {
TileScaling::World => Vec2::ONE,
TileScaling::Pixels => self.layout.pixels_per_tile.as_vec2(),
}
}
pub fn screen_to_world(&self, screen_pos: Vec2) -> Option<Vec2> {
if let Some(viewport_size) = self.viewport_size {
let screen_pos = screen_pos - self.viewport_pos;
let ndc = (screen_pos / viewport_size) * 2.0 - Vec2::ONE;
let world_pos = self.ndc_to_world.project_point3(ndc.extend(-1.0));
Some(world_pos.truncate())
} else {
None
}
}
}
#[allow(clippy::type_complexity)]
fn update_from_terminal(
mut q_term: Query<
(&mut ToWorld, &Terminal, &GlobalTransform, &TerminalLayout),
Or<(Changed<Terminal>, Changed<TerminalLayout>)>,
>,
) {
for (mut to_world, term, transform, layout) in q_term.iter_mut() {
to_world.term_size = term.size();
to_world.layout = layout.clone();
to_world.term_pos = transform.translation();
}
}
#[allow(clippy::type_complexity)]
fn update_from_camera(
q_cam: Query<
(Entity, &Camera, &GlobalTransform),
Or<(Changed<Camera>, Changed<GlobalTransform>)>,
>,
mut q_to_world: Query<&mut ToWorld>,
windows: Res<Windows>,
images: Res<Assets<Image>>,
) {
if q_cam.is_empty() {
return;
}
for mut tw in q_to_world.iter_mut() {
if tw.camera_entity.is_none() {
tw.camera_entity = Some(q_cam.iter().next().unwrap().0);
}
for (cam_entity, cam, t) in q_cam.iter() {
if cam_entity != tw.camera_entity.unwrap() {
continue;
}
tw.camera_pos = t.translation();
tw.ndc_to_world = t.compute_matrix() * cam.projection_matrix().inverse();
if let Some(vp) = &cam.viewport {
tw.viewport_pos = vp.physical_position.as_vec2();
tw.viewport_size = Some(vp.physical_size.as_vec2());
} else {
tw.viewport_pos = Vec2::ZERO;
let res = match &cam.target {
RenderTarget::Window(win_id) => {
windows
.get(*win_id)
.map(|window| Vec2::new(window.width(), window.height()))
}
RenderTarget::Image(image) => {
images.get(image).map(|image| image.size())
}
};
tw.viewport_size = res;
}
}
}
}