use retroglyph_core::grid::{HasSize, Pos, Rect, Size};
mod transform;
#[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.set_origin(self.origin);
}
pub fn set_world(&mut self, world: Size) {
self.world = world;
self.origin = Rect::from_tl_size(self.origin, self.viewport.size())
.clamp_within(world.to_rect())
.top_left();
}
pub fn set_viewport_fitted(&mut self, viewport: Rect) {
let width = viewport.width().min(self.world.width());
let height = viewport.height().min(self.world.height());
let x = viewport
.left()
.saturating_add((viewport.width() - width) / 2);
let y = viewport
.top()
.saturating_add((viewport.height() - height) / 2);
self.set_viewport(Rect::new(x, y, width, height));
}
pub fn center_on(&mut self, target: Pos) {
self.set_origin(Pos::new(
target.x.saturating_sub(self.viewport.width() / 2),
target.y.saturating_sub(self.viewport.height() / 2),
));
}
pub fn set_origin(&mut self, origin: Pos) {
self.origin = Rect::from_tl_size(origin, self.viewport.size())
.clamp_within(self.world.to_rect())
.top_left();
}
pub fn scroll_by(&mut self, dx: i32, dy: i32) {
self.set_origin(self.origin.saturating_add_signed(ixy::Pos::new(dx, dy)));
}
}
#[cfg(test)]
pub(super) const fn cam() -> Camera {
Camera::new(Rect::new(0, 0, 10, 10), Size::new(100, 100))
}
#[cfg(test)]
mod tests {
use super::*;
#[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 world_smaller_than_viewport_pins_origin() {
let mut c = Camera::new(Rect::new(2, 2, 20, 20), Size::new(5, 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)));
assert_eq!(c.world_to_screen(Pos::new(7, 7)), None);
}
#[test]
fn set_world_re_clamps_the_origin_when_the_world_shrinks() {
let mut c = cam();
c.center_on(Pos::new(50, 50));
assert_eq!(c.origin(), Pos::new(45, 45));
c.set_world(Size::new(20, 20));
assert_eq!(c.world(), Size::new(20, 20));
assert_eq!(c.origin(), Pos::new(10, 10));
}
#[test]
fn set_world_leaves_an_in_bounds_origin_unchanged_when_the_world_grows() {
let mut c = cam();
c.center_on(Pos::new(50, 50));
assert_eq!(c.origin(), Pos::new(45, 45));
c.set_world(Size::new(200, 200));
assert_eq!(c.world(), Size::new(200, 200));
assert_eq!(c.origin(), Pos::new(45, 45));
}
#[test]
fn set_world_pins_origin_to_zero_when_the_new_world_is_smaller_than_the_viewport() {
let mut c = cam();
c.center_on(Pos::new(50, 50));
c.set_world(Size::new(5, 5));
assert_eq!(c.origin(), Pos::new(0, 0));
}
#[test]
fn set_viewport_fitted_shrinks_and_centers_a_world_smaller_on_both_axes() {
let mut c = Camera::new(Rect::new(0, 0, 1, 1), Size::new(5, 5));
c.set_viewport_fitted(Rect::new(2, 2, 20, 20));
assert_eq!(c.viewport(), Rect::new(9, 9, 5, 5));
assert_eq!(c.origin(), Pos::new(0, 0));
assert_eq!(c.visible_bounds(), Rect::new(0, 0, 5, 5));
assert_eq!(c.world_to_screen(Pos::new(0, 0)), Some(Pos::new(9, 9)));
}
#[test]
fn set_viewport_fitted_shrinks_only_the_axis_that_is_smaller() {
let mut c = Camera::new(Rect::new(0, 0, 1, 1), Size::new(5, 100));
c.set_viewport_fitted(Rect::new(0, 0, 20, 10));
assert_eq!(c.viewport(), Rect::new(7, 0, 5, 10));
}
#[test]
fn set_viewport_fitted_rounds_odd_slack_toward_the_right_and_bottom() {
let mut c = Camera::new(Rect::new(0, 0, 1, 1), Size::new(4, 4));
c.set_viewport_fitted(Rect::new(0, 0, 9, 9));
assert_eq!(c.viewport(), Rect::new(2, 2, 4, 4));
}
#[test]
fn set_viewport_reclamps_the_origin_when_the_viewport_grows() {
let mut c = cam(); c.center_on(Pos::new(99, 99)); c.set_viewport(Rect::new(0, 0, 40, 40));
assert_eq!(c.origin(), Pos::new(60, 60)); assert_eq!(c.visible_bounds(), Rect::new(60, 60, 40, 40));
}
#[test]
fn zero_size_camera_is_inert() {
let c = Camera::new(Rect::new(0, 0, 0, 0), Size::new(0, 0));
assert_eq!(c.visible_bounds(), Rect::new(0, 0, 0, 0));
assert_eq!(c.cells().count(), 0);
assert_eq!(c.world_to_screen(Pos::new(0, 0)), None);
assert_eq!(c.screen_to_world(Pos::new(0, 0)), None);
let zero_world = Camera::new(Rect::new(0, 0, 10, 10), Size::new(0, 0));
assert_eq!(zero_world.visible_bounds(), Rect::new(0, 0, 0, 0));
assert_eq!(zero_world.cells().count(), 0);
assert_eq!(zero_world.screen_to_world(Pos::new(0, 0)), None);
}
#[test]
fn set_viewport_fitted_matches_set_viewport_when_the_world_is_not_smaller() {
let mut a = Camera::new(Rect::new(0, 0, 1, 1), Size::new(100, 100));
a.set_viewport_fitted(Rect::new(0, 0, 10, 10));
let mut b = Camera::new(Rect::new(0, 0, 1, 1), Size::new(100, 100));
b.set_viewport(Rect::new(0, 0, 10, 10));
assert_eq!(a.viewport(), b.viewport());
assert_eq!(a.origin(), b.origin());
}
#[test]
fn set_viewport_fitted_saturates_instead_of_overflowing() {
let mut c = Camera::new(Rect::new(0, 0, 1, 1), Size::new(5, 5));
c.set_viewport_fitted(Rect::new(65_530, 0, 1_000, 10));
assert_eq!(c.viewport(), Rect::new(u16::MAX, 2, 5, 5));
}
#[test]
fn scroll_by_moves_the_origin_by_the_delta() {
let mut c = cam();
c.scroll_by(5, 3);
assert_eq!(c.origin(), Pos::new(5, 3));
c.scroll_by(-2, 1);
assert_eq!(c.origin(), Pos::new(3, 4));
}
#[test]
fn scroll_by_clamps_at_the_low_edge_without_overshooting() {
let mut c = cam();
c.scroll_by(-1000, -1000);
assert_eq!(c.origin(), Pos::new(0, 0));
}
#[test]
fn scroll_by_clamps_at_the_high_edge_without_overshooting() {
let mut c = cam(); c.scroll_by(1000, 1000);
assert_eq!(c.origin(), Pos::new(90, 90));
}
#[test]
fn scroll_by_has_no_dead_zone_reversing_direction_past_an_edge() {
let mut c = cam(); c.scroll_by(1000, 0); assert_eq!(c.origin(), Pos::new(90, 0));
c.scroll_by(-1, 0); assert_eq!(
c.origin(),
Pos::new(89, 0),
"a single reversed cell must move the origin"
);
}
#[test]
fn set_origin_places_the_origin_exactly_when_in_bounds() {
let mut c = cam();
c.set_origin(Pos::new(12, 34));
assert_eq!(c.origin(), Pos::new(12, 34));
}
#[test]
fn set_origin_clamps_to_the_world_edge() {
let mut c = cam(); c.set_origin(Pos::new(95, 200));
assert_eq!(c.origin(), Pos::new(90, 90));
}
#[test]
fn set_origin_never_underflows_when_the_viewport_exceeds_the_world() {
let mut c = Camera::new(Rect::new(0, 0, 20, 20), Size::new(5, 5));
c.set_origin(Pos::new(3, 3));
assert_eq!(c.origin(), Pos::new(0, 0));
}
#[test]
fn center_on_and_set_origin_agree_on_the_clamped_result() {
let mut a = cam();
a.center_on(Pos::new(99, 99));
let mut b = cam();
b.set_origin(Pos::new(94, 94));
assert_eq!(a.origin(), b.origin());
}
}