use crate::grid::{Pos, Rect, Size};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Camera {
viewport: Rect,
world: Size,
origin: Pos,
}
impl Camera {
#[must_use]
pub const fn new(viewport: Rect, world: Size) -> Self {
Self {
viewport,
world,
origin: Pos::new(0, 0),
}
}
#[must_use]
pub const fn viewport(&self) -> Rect {
self.viewport
}
#[must_use]
pub const fn world(&self) -> Size {
self.world
}
#[must_use]
pub const fn origin(&self) -> Pos {
self.origin
}
pub fn set_viewport(&mut self, viewport: Rect) {
self.viewport = viewport;
self.origin = Pos::new(
self.origin
.x
.min(max_origin(viewport.width(), self.world.width)),
self.origin
.y
.min(max_origin(viewport.height(), self.world.height)),
);
}
pub fn center_on(&mut self, target: Pos) {
self.origin = Pos::new(
center_axis(target.x, self.viewport.width(), self.world.width),
center_axis(target.y, self.viewport.height(), self.world.height),
);
}
#[must_use]
pub fn visible_bounds(&self) -> Rect {
let w = self
.viewport
.width()
.min(self.world.width.saturating_sub(self.origin.x));
let h = self
.viewport
.height()
.min(self.world.height.saturating_sub(self.origin.y));
Rect::new(self.origin.x, self.origin.y, w, h)
}
#[must_use]
pub const fn world_to_screen(&self, world: Pos) -> Option<Pos> {
if world.x < self.origin.x || world.y < self.origin.y {
return None;
}
let dx = world.x - self.origin.x;
let dy = world.y - self.origin.y;
if dx >= self.viewport.width() || dy >= self.viewport.height() {
return None;
}
Some(Pos::new(
self.viewport.left() + dx,
self.viewport.top() + dy,
))
}
#[must_use]
pub fn screen_to_world(&self, screen: Pos) -> Option<Pos> {
if !self.viewport.contains_pos(screen) {
return None;
}
let wx = self.origin.x + (screen.x - self.viewport.left());
let wy = self.origin.y + (screen.y - self.viewport.top());
if wx >= self.world.width || wy >= self.world.height {
return None;
}
Some(Pos::new(wx, wy))
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub fn cells(&self) -> impl Iterator<Item = (Pos, Pos)> + '_ {
let vis = self.visible_bounds();
let vp = self.viewport;
let origin = self.origin;
(vis.top()..vis.bottom()).flat_map(move |wy| {
(vis.left()..vis.right()).map(move |wx| {
let screen = Pos::new(vp.left() + (wx - origin.x), vp.top() + (wy - origin.y));
(Pos::new(wx, wy), screen)
})
})
}
}
const fn max_origin(view: u16, world: u16) -> u16 {
world.saturating_sub(view)
}
fn center_axis(target: u16, view: u16, world: u16) -> u16 {
target.saturating_sub(view / 2).min(max_origin(view, world))
}
#[cfg(test)]
mod tests {
use super::*;
fn cam() -> Camera {
Camera::new(
Rect::new(0, 0, 10, 10),
Size {
width: 100,
height: 100,
},
)
}
#[test]
fn centers_in_the_interior() {
let mut c = cam();
c.center_on(Pos::new(50, 50));
assert_eq!(c.origin(), Pos::new(45, 45));
assert_eq!(c.world_to_screen(Pos::new(50, 50)), Some(Pos::new(5, 5)));
assert_eq!(c.screen_to_world(Pos::new(5, 5)), Some(Pos::new(50, 50)));
}
#[test]
fn clamps_at_the_low_edge() {
let mut c = cam();
c.center_on(Pos::new(1, 1));
assert_eq!(c.origin(), Pos::new(0, 0));
assert_eq!(c.world_to_screen(Pos::new(1, 1)), Some(Pos::new(1, 1)));
}
#[test]
fn clamps_at_the_high_edge() {
let mut c = cam();
c.center_on(Pos::new(99, 99));
assert_eq!(c.origin(), Pos::new(90, 90));
assert_eq!(c.world_to_screen(Pos::new(99, 99)), Some(Pos::new(9, 9)));
}
#[test]
fn offscreen_positions_return_none() {
let mut c = cam();
c.center_on(Pos::new(50, 50)); assert_eq!(c.world_to_screen(Pos::new(44, 50)), None);
assert_eq!(c.world_to_screen(Pos::new(55, 50)), None);
}
#[test]
fn world_smaller_than_viewport_pins_origin() {
let mut c = Camera::new(
Rect::new(2, 2, 20, 20),
Size {
width: 5,
height: 5,
},
);
c.center_on(Pos::new(3, 3));
assert_eq!(c.origin(), Pos::new(0, 0));
let visible = c.visible_bounds();
assert_eq!((visible.width(), visible.height()), (5, 5));
assert_eq!(c.world_to_screen(Pos::new(0, 0)), Some(Pos::new(2, 2)));
}
#[test]
fn cells_yields_visible_world_and_screen_pairs() {
let mut c = cam();
c.center_on(Pos::new(50, 50));
let pairs: Vec<_> = c.cells().collect();
assert_eq!(pairs.len(), 100); assert_eq!(pairs[0], (Pos::new(45, 45), Pos::new(0, 0)));
assert_eq!(pairs[99], (Pos::new(54, 54), Pos::new(9, 9)));
}
}