use glam::Vec3;
use crate::ecs::Resource;
use crate::sceneobjects::cameras::Camera;
use crate::ui::Rect;
const MINIMAP_HEIGHT: f32 = 0.25;
const MINIMAP_MARGIN: f32 = 0.02;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct View {
pub rect: Rect,
pub camera: Camera,
pub tiles: bool,
}
impl View {
pub fn new(rect: Rect, camera: Camera) -> Self {
Self {
rect,
camera,
tiles: true,
}
}
pub fn overlay(rect: Rect, camera: Camera) -> Self {
Self {
rect,
camera,
tiles: false,
}
}
pub fn aspect(&self) -> f32 {
self.rect.width / self.rect.height.max(1.0)
}
pub fn project(&self, world: Vec3) -> Option<(f32, f32)> {
let (x, y) = self.project_unclipped(world)?;
self.rect.contains(x, y).then_some((x, y))
}
pub fn project_unclipped(&self, world: Vec3) -> Option<(f32, f32)> {
let clip = self.camera.view_proj(self.aspect()) * world.extend(1.0);
if clip.w <= 0.0 || clip.z < 0.0 {
return None;
}
let ndc = clip.truncate() / clip.w;
if ndc.z > 1.0 {
return None;
}
let x = self.rect.x + (ndc.x + 1.0) * 0.5 * self.rect.width;
let y = self.rect.y + (1.0 - ndc.y) * 0.5 * self.rect.height;
Some((x, y))
}
pub fn is_near(&self, x: f32, y: f32, margin: f32) -> bool {
x >= self.rect.x - margin
&& x <= self.rect.x + self.rect.width + margin
&& y >= self.rect.y - margin
&& y <= self.rect.y + self.rect.height + margin
}
}
#[derive(Resource, Clone, Debug, PartialEq)]
pub struct Views(Vec<View>);
impl Default for Views {
fn default() -> Self {
Self::one(Camera::default())
}
}
impl Views {
pub fn one(camera: Camera) -> Self {
Self(vec![View::new(Rect::new(0.0, 0.0, 0.0, 0.0), camera)])
}
pub fn split(left: Camera, right: Camera, width: f32, height: f32) -> Self {
let half = width * 0.5;
Self(vec![
View::new(Rect::new(0.0, 0.0, half, height), left),
View::new(Rect::new(half, 0.0, width - half, height), right),
])
}
pub fn with_minimap(mut self, camera: Camera) -> Self {
let frame = self.frame();
let side = frame.height * MINIMAP_HEIGHT;
let margin = frame.height * MINIMAP_MARGIN;
self.0.push(View::overlay(
Rect::new(
frame.center_x() - side * 0.5,
frame.height - side - margin,
side,
side,
),
camera,
));
self
}
fn frame(&self) -> Rect {
self.0.iter().filter(|view| view.tiles).fold(
Rect::new(0.0, 0.0, 0.0, 0.0),
|frame, view| {
Rect::new(
0.0,
0.0,
frame.width.max(view.rect.x + view.rect.width),
frame.height.max(view.rect.y + view.rect.height),
)
},
)
}
pub fn iter(&self) -> impl Iterator<Item = &View> {
self.0.iter()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn camera(&self) -> Camera {
self.0
.first()
.map(|view| view.camera)
.unwrap_or_else(Camera::default)
}
pub fn set_camera(&mut self, camera: Camera) -> bool {
match self.0.as_mut_slice() {
[only] => {
only.camera = camera;
true
}
_ => false,
}
}
pub fn at(&self, x: f32, y: f32) -> Option<&View> {
self.0.iter().rev().find(|view| view.rect.contains(x, y))
}
pub fn fit(&mut self, width: f32, height: f32) {
let frame = self.frame();
let (was_width, was_height) = (frame.width, frame.height);
for view in &mut self.0 {
if was_width <= 0.0 || was_height <= 0.0 {
view.rect = Rect::new(0.0, 0.0, width, height);
continue;
}
let (sx, sy) = (width / was_width, height / was_height);
view.rect = Rect::new(
view.rect.x * sx,
view.rect.y * sy,
view.rect.width * sx,
view.rect.height * sy,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn camera(x: f32) -> Camera {
Camera::looking_at(glam::Vec3::new(x, 0.0, 0.0), glam::Vec3::ZERO)
}
#[test]
fn one_view_fills_the_frame() {
let mut views = Views::one(camera(0.0));
views.fit(1920.0, 1080.0);
assert_eq!(views.len(), 1);
let view = views.iter().next().unwrap();
assert_eq!(view.rect, Rect::new(0.0, 0.0, 1920.0, 1080.0));
assert!(view.tiles, "it is the frame");
}
#[test]
fn a_split_screen_covers_the_frame_exactly() {
let mut views = Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0);
views.fit(1920.0, 1080.0);
let halves: Vec<Rect> = views.iter().map(|view| view.rect).collect();
assert_eq!(halves[0], Rect::new(0.0, 0.0, 960.0, 1080.0));
assert_eq!(halves[1], Rect::new(960.0, 0.0, 960.0, 1080.0));
assert_eq!(
halves[0].x + halves[0].width,
halves[1].x,
"they have to meet exactly",
);
assert_eq!(halves[1].x + halves[1].width, 1920.0, "and reach the edge");
}
#[test]
fn an_odd_width_still_covers_every_pixel() {
let mut views = Views::split(camera(-1.0), camera(1.0), 1921.0, 1080.0);
views.fit(1921.0, 1080.0);
let halves: Vec<Rect> = views.iter().map(|view| view.rect).collect();
assert_eq!(halves[0].x + halves[0].width, halves[1].x);
assert_eq!(halves[1].x + halves[1].width, 1921.0);
}
#[test]
fn each_half_knows_it_is_half_as_wide() {
let mut views = Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0);
views.fit(1920.0, 1080.0);
for view in views.iter() {
assert!(
(view.aspect() - 960.0 / 1080.0).abs() < 1e-5,
"a half-width view is not the frame's shape: {}",
view.aspect(),
);
}
}
#[test]
fn the_minimap_sits_over_the_join_at_the_bottom() {
let mut views =
Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
views.fit(1920.0, 1080.0);
let map = views.iter().last().unwrap();
assert!(!map.tiles, "it is laid over the two, not cut into them");
assert!(
(map.rect.center_x() - 960.0).abs() < 1e-4,
"centred on the join: {:?}",
map.rect,
);
assert!(
map.rect.y + map.rect.height < 1080.0,
"floating off the bottom edge rather than glued to it",
);
assert_eq!(map.rect.width, map.rect.height, "square, like the ground");
}
#[test]
fn the_minimap_overlaps_both_players() {
let mut views =
Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
views.fit(1920.0, 1080.0);
let rects: Vec<Rect> = views.iter().map(|view| view.rect).collect();
let (left, right, map) = (rects[0], rects[1], rects[2]);
assert!(map.x < left.x + left.width, "it reaches into the left half");
assert!(map.x + map.width > right.x, "and into the right one");
}
#[test]
fn a_resize_keeps_the_layout() {
let mut views =
Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
views.fit(1920.0, 1080.0);
views.fit(1280.0, 720.0);
let rects: Vec<Rect> = views.iter().map(|view| view.rect).collect();
assert_eq!(rects[0], Rect::new(0.0, 0.0, 640.0, 720.0));
assert_eq!(rects[1], Rect::new(640.0, 0.0, 640.0, 720.0));
assert!(
(rects[2].center_x() - 640.0).abs() < 1e-4,
"the map stays over the join: {:?}",
rects[2],
);
}
#[test]
fn a_point_belongs_to_the_view_drawn_last() {
let mut views =
Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
views.fit(1920.0, 1080.0);
let map = views.iter().last().unwrap().rect;
let over_map = views.at(map.center_x(), map.center_y()).unwrap().rect;
assert_eq!(over_map, map, "the map wins where it covers the players");
let left = views.at(100.0, 100.0).unwrap().rect;
assert_eq!(left.x, 0.0, "and away from it the player view answers");
assert!(views.at(-1.0, 100.0).is_none(), "off the frame is nobody's");
}
fn facing() -> (View, View) {
let from_z = Camera::looking_at(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO);
let mut views = Views::split(camera(-10.0), from_z, 1920.0, 1080.0);
views.fit(1920.0, 1080.0);
let mut views = views.iter().copied();
(views.next().unwrap(), views.next().unwrap())
}
#[test]
fn what_a_camera_looks_at_lands_in_the_middle_of_its_own_view() {
let (left, right) = facing();
let (x, y) = left.project(Vec3::ZERO).unwrap();
assert!(
(x - 480.0).abs() < 1e-2 && (y - 540.0).abs() < 1e-2,
"({x}, {y})"
);
let (x, y) = right.project(Vec3::ZERO).unwrap();
assert!(
(x - 1440.0).abs() < 1e-2 && (y - 540.0).abs() < 1e-2,
"({x}, {y})"
);
}
#[test]
fn up_in_the_world_is_up_the_screen() {
let (left, _) = facing();
let (x, y) = left.project(Vec3::new(0.0, 1.0, 0.0)).unwrap();
assert!(y < 540.0, "above the target came out at y {y}");
assert!((x - 480.0).abs() < 1e-2, "and dead ahead: x {x}");
}
#[test]
fn a_point_behind_the_camera_is_nowhere_on_screen() {
let (left, _) = facing();
assert_eq!(left.project(Vec3::new(-20.0, 0.0, 0.0)), None);
assert_eq!(left.project(Vec3::new(-10.0, 0.0, 0.0)), None);
}
#[test]
fn a_point_past_the_far_plane_is_nowhere_on_screen_either() {
let (left, _) = facing();
let eye = left.camera.eye.x;
let just_short = Vec3::new(eye + left.camera.far * 0.99, 0.0, 0.0);
let just_past = Vec3::new(eye + left.camera.far * 1.01, 0.0, 0.0);
assert!(left.project(just_short).is_some());
assert_eq!(left.project(just_past), None);
}
#[test]
fn a_point_in_the_other_players_view_is_none_through_this_one() {
let (left, right) = facing();
let ahead_of_right = Vec3::new(0.0, 0.0, 5.0);
let (x, y) = right.project(ahead_of_right).unwrap();
assert!(
(x - 1440.0).abs() < 1e-2 && (y - 540.0).abs() < 1e-2,
"({x}, {y})"
);
assert_eq!(left.project(ahead_of_right), None);
}
#[test]
fn the_edge_a_projection_falls_off_is_the_views_own() {
let (left, _) = facing();
let half_width = (45f32.to_radians() * 0.5).tan() * left.aspect() * 10.0;
let inside = Vec3::new(0.0, 0.0, half_width * 0.99);
let outside = Vec3::new(0.0, 0.0, half_width * 1.01);
let (x, _) = left.project(inside).unwrap();
assert!(x > 940.0 && x <= 960.0, "{x}");
assert_eq!(left.project(outside), None);
}
}