1use crate::grid::{Pos, Rect, Size};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Camera {
34 viewport: Rect,
35 world: Size,
36 origin: Pos,
37}
38
39impl Camera {
40 #[must_use]
44 pub const fn new(viewport: Rect, world: Size) -> Self {
45 Self {
46 viewport,
47 world,
48 origin: Pos::new(0, 0),
49 }
50 }
51
52 #[must_use]
54 pub const fn viewport(&self) -> Rect {
55 self.viewport
56 }
57
58 #[must_use]
60 pub const fn world(&self) -> Size {
61 self.world
62 }
63
64 #[must_use]
66 pub const fn origin(&self) -> Pos {
67 self.origin
68 }
69
70 pub fn set_viewport(&mut self, viewport: Rect) {
73 self.viewport = viewport;
74 self.origin = Pos::new(
75 self.origin
76 .x
77 .min(max_origin(viewport.width(), self.world.width)),
78 self.origin
79 .y
80 .min(max_origin(viewport.height(), self.world.height)),
81 );
82 }
83
84 pub fn center_on(&mut self, target: Pos) {
87 self.origin = Pos::new(
88 center_axis(target.x, self.viewport.width(), self.world.width),
89 center_axis(target.y, self.viewport.height(), self.world.height),
90 );
91 }
92
93 #[must_use]
95 pub fn visible_bounds(&self) -> Rect {
96 let w = self
97 .viewport
98 .width()
99 .min(self.world.width.saturating_sub(self.origin.x));
100 let h = self
101 .viewport
102 .height()
103 .min(self.world.height.saturating_sub(self.origin.y));
104 Rect::new(self.origin.x, self.origin.y, w, h)
105 }
106
107 #[must_use]
110 pub const fn world_to_screen(&self, world: Pos) -> Option<Pos> {
111 if world.x < self.origin.x || world.y < self.origin.y {
112 return None;
113 }
114 let dx = world.x - self.origin.x;
115 let dy = world.y - self.origin.y;
116 if dx >= self.viewport.width() || dy >= self.viewport.height() {
117 return None;
118 }
119 Some(Pos::new(
120 self.viewport.left() + dx,
121 self.viewport.top() + dy,
122 ))
123 }
124
125 #[must_use]
128 pub fn screen_to_world(&self, screen: Pos) -> Option<Pos> {
129 if !self.viewport.contains_pos(screen) {
130 return None;
131 }
132 let wx = self.origin.x + (screen.x - self.viewport.left());
133 let wy = self.origin.y + (screen.y - self.viewport.top());
134 if wx >= self.world.width || wy >= self.world.height {
135 return None;
136 }
137 Some(Pos::new(wx, wy))
138 }
139
140 #[must_use = "iterators are lazy and do nothing unless consumed"]
144 pub fn cells(&self) -> impl Iterator<Item = (Pos, Pos)> + '_ {
145 let vis = self.visible_bounds();
146 let vp = self.viewport;
147 let origin = self.origin;
148 (vis.top()..vis.bottom()).flat_map(move |wy| {
149 (vis.left()..vis.right()).map(move |wx| {
150 let screen = Pos::new(vp.left() + (wx - origin.x), vp.top() + (wy - origin.y));
151 (Pos::new(wx, wy), screen)
152 })
153 })
154 }
155}
156
157const fn max_origin(view: u16, world: u16) -> u16 {
160 world.saturating_sub(view)
161}
162
163fn center_axis(target: u16, view: u16, world: u16) -> u16 {
165 target.saturating_sub(view / 2).min(max_origin(view, world))
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn cam() -> Camera {
173 Camera::new(
174 Rect::new(0, 0, 10, 10),
175 Size {
176 width: 100,
177 height: 100,
178 },
179 )
180 }
181
182 #[test]
183 fn centers_in_the_interior() {
184 let mut c = cam();
185 c.center_on(Pos::new(50, 50));
186 assert_eq!(c.origin(), Pos::new(45, 45));
187 assert_eq!(c.world_to_screen(Pos::new(50, 50)), Some(Pos::new(5, 5)));
188 assert_eq!(c.screen_to_world(Pos::new(5, 5)), Some(Pos::new(50, 50)));
189 }
190
191 #[test]
192 fn clamps_at_the_low_edge() {
193 let mut c = cam();
194 c.center_on(Pos::new(1, 1));
195 assert_eq!(c.origin(), Pos::new(0, 0));
196 assert_eq!(c.world_to_screen(Pos::new(1, 1)), Some(Pos::new(1, 1)));
197 }
198
199 #[test]
200 fn clamps_at_the_high_edge() {
201 let mut c = cam();
202 c.center_on(Pos::new(99, 99));
203 assert_eq!(c.origin(), Pos::new(90, 90));
205 assert_eq!(c.world_to_screen(Pos::new(99, 99)), Some(Pos::new(9, 9)));
206 }
207
208 #[test]
209 fn offscreen_positions_return_none() {
210 let mut c = cam();
211 c.center_on(Pos::new(50, 50)); assert_eq!(c.world_to_screen(Pos::new(44, 50)), None);
213 assert_eq!(c.world_to_screen(Pos::new(55, 50)), None);
214 }
215
216 #[test]
217 fn world_smaller_than_viewport_pins_origin() {
218 let mut c = Camera::new(
219 Rect::new(2, 2, 20, 20),
220 Size {
221 width: 5,
222 height: 5,
223 },
224 );
225 c.center_on(Pos::new(3, 3));
226 assert_eq!(c.origin(), Pos::new(0, 0));
227 let visible = c.visible_bounds();
228 assert_eq!((visible.width(), visible.height()), (5, 5));
229 assert_eq!(c.world_to_screen(Pos::new(0, 0)), Some(Pos::new(2, 2)));
231 }
232
233 #[test]
234 fn cells_yields_visible_world_and_screen_pairs() {
235 let mut c = cam();
236 c.center_on(Pos::new(50, 50));
237 let pairs: Vec<_> = c.cells().collect();
238 assert_eq!(pairs.len(), 100); assert_eq!(pairs[0], (Pos::new(45, 45), Pos::new(0, 0)));
240 assert_eq!(pairs[99], (Pos::new(54, 54), Pos::new(9, 9)));
241 }
242}