1use crate::painter::Painter;
2use crate::sprite::{BlendMode, SpriteInstance};
3use crate::texture::TextureId;
4
5pub struct Tilemap {
13 pub atlas: TextureId,
14 pub atlas_size: [u32; 2],
15 pub tile_size: [u32; 2],
16 pub width: u32,
17 pub height: u32,
18 pub origin: [f32; 2],
20 pub tiles: Vec<u16>,
21 pub layer: i16,
22 chunk_size: u32,
23 chunk_cols: u32,
24 chunks: Vec<Chunk>,
25 last_origin: [f32; 2],
26}
27
28struct Chunk {
30 instances: Vec<SpriteInstance>,
31 dirty: bool,
32}
33
34impl Tilemap {
35 pub fn new(
36 atlas: TextureId,
37 atlas_size: [u32; 2],
38 tile_size: [u32; 2],
39 width: u32,
40 height: u32,
41 ) -> Self {
42 Self::with_chunk_size(atlas, atlas_size, tile_size, width, height, 32)
43 }
44
45 pub fn with_chunk_size(
47 atlas: TextureId,
48 atlas_size: [u32; 2],
49 tile_size: [u32; 2],
50 width: u32,
51 height: u32,
52 chunk_size: u32,
53 ) -> Self {
54 let chunk_size = chunk_size.max(1);
55 let chunk_cols = width.div_ceil(chunk_size);
56 let chunk_rows = height.div_ceil(chunk_size);
57 let chunks = (0..chunk_cols * chunk_rows)
58 .map(|_| Chunk {
59 instances: Vec::new(),
60 dirty: true,
61 })
62 .collect();
63 Self {
64 atlas,
65 atlas_size,
66 tile_size,
67 width,
68 height,
69 origin: [0.0, 0.0],
70 tiles: vec![0; (width * height) as usize],
71 layer: 0,
72 chunk_size,
73 chunk_cols,
74 chunks,
75 last_origin: [0.0, 0.0],
76 }
77 }
78
79 pub fn set(&mut self, x: u32, y: u32, tile: u16) {
80 if x < self.width && y < self.height {
81 self.tiles[(y * self.width + x) as usize] = tile;
82 let ci = (y / self.chunk_size) * self.chunk_cols + (x / self.chunk_size);
83 self.chunks[ci as usize].dirty = true;
84 }
85 }
86
87 pub fn get(&self, x: u32, y: u32) -> u16 {
88 if x < self.width && y < self.height {
89 self.tiles[(y * self.width + x) as usize]
90 } else {
91 0
92 }
93 }
94
95 pub fn mark_all_dirty(&mut self) {
100 for c in &mut self.chunks {
101 c.dirty = true;
102 }
103 }
104
105 fn tiles_per_row(&self) -> u32 {
106 (self.atlas_size[0] / self.tile_size[0]).max(1)
107 }
108
109 fn uv_for(&self, tile: u16) -> ([f32; 2], [f32; 2]) {
111 let idx = (tile - 1) as u32;
112 let cols = self.tiles_per_row();
113 let cx = idx % cols;
114 let cy = idx / cols;
115 let u0 = (cx * self.tile_size[0]) as f32 / self.atlas_size[0] as f32;
116 let v0 = (cy * self.tile_size[1]) as f32 / self.atlas_size[1] as f32;
117 let u1 = ((cx + 1) * self.tile_size[0]) as f32 / self.atlas_size[0] as f32;
118 let v1 = ((cy + 1) * self.tile_size[1]) as f32 / self.atlas_size[1] as f32;
119 ([u0, v0], [u1, v1])
120 }
121
122 fn chunk_cells(&self, ci: usize) -> (u32, u32, u32, u32) {
124 let ccx = ci as u32 % self.chunk_cols;
125 let ccy = ci as u32 / self.chunk_cols;
126 let x0 = ccx * self.chunk_size;
127 let y0 = ccy * self.chunk_size;
128 (
129 x0,
130 y0,
131 (x0 + self.chunk_size).min(self.width),
132 (y0 + self.chunk_size).min(self.height),
133 )
134 }
135
136 fn chunk_aabb(&self, ci: usize) -> ([f32; 2], [f32; 2]) {
138 let (x0, y0, x1, y1) = self.chunk_cells(ci);
139 let tw = self.tile_size[0] as f32;
140 let th = self.tile_size[1] as f32;
141 (
142 [
143 self.origin[0] + x0 as f32 * tw,
144 self.origin[1] + y0 as f32 * th,
145 ],
146 [
147 self.origin[0] + x1 as f32 * tw,
148 self.origin[1] + y1 as f32 * th,
149 ],
150 )
151 }
152
153 fn build_chunk(&self, ci: usize) -> Vec<SpriteInstance> {
154 let (x0, y0, x1, y1) = self.chunk_cells(ci);
155 let tw = self.tile_size[0] as f32;
156 let th = self.tile_size[1] as f32;
157 let mut instances = Vec::new();
158 for y in y0..y1 {
159 for x in x0..x1 {
160 let t = self.tiles[(y * self.width + x) as usize];
161 if t == 0 {
162 continue;
163 }
164 let (uv_min, uv_max) = self.uv_for(t);
165 let pos = [
166 self.origin[0] + x as f32 * tw,
167 self.origin[1] + y as f32 * th,
168 ];
169 instances.push(SpriteInstance::at(pos, [tw, th]).with_uv(uv_min, uv_max));
170 }
171 }
172 instances
173 }
174
175 pub fn draw(&mut self, painter: &mut Painter) {
178 if self.origin != self.last_origin {
180 self.mark_all_dirty();
181 self.last_origin = self.origin;
182 }
183 for ci in 0..self.chunks.len() {
184 if self.chunks[ci].dirty {
185 self.chunks[ci].instances = self.build_chunk(ci);
186 self.chunks[ci].dirty = false;
187 }
188 }
189
190 let (vmin, vmax) = painter.visible_rect();
191 for ci in 0..self.chunks.len() {
192 let (cmin, cmax) = self.chunk_aabb(ci);
193 if !rects_intersect(cmin, cmax, vmin, vmax) {
194 continue;
195 }
196 for inst in &self.chunks[ci].instances {
197 painter.sprite_ex(self.atlas, *inst, self.layer, BlendMode::Alpha);
198 }
199 }
200 }
201}
202
203fn rects_intersect(amin: [f32; 2], amax: [f32; 2], bmin: [f32; 2], bmax: [f32; 2]) -> bool {
205 amin[0] < bmax[0] && amax[0] > bmin[0] && amin[1] < bmax[1] && amax[1] > bmin[1]
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 fn map() -> Tilemap {
213 Tilemap::new(TextureId(0), [64, 64], [16, 16], 256, 256)
215 }
216
217 #[test]
218 fn chunk_grid_dimensions() {
219 let m = map();
220 assert_eq!(m.chunk_cols, 8);
221 assert_eq!(m.chunks.len(), 64); }
223
224 #[test]
225 fn set_marks_only_the_owning_chunk() {
226 let mut m = map();
227 m.mark_all_dirty();
228 for c in &mut m.chunks {
229 c.dirty = false;
230 }
231 m.set(40, 70, 1); assert!(m.chunks[17].dirty);
233 assert_eq!(m.chunks.iter().filter(|c| c.dirty).count(), 1);
234 }
235
236 #[test]
237 fn culling_skips_offscreen_chunks() {
238 let m = map();
239 let (vmin, vmax) = ([0.0, 0.0], [32.0, 32.0]);
241 let visible = (0..m.chunks.len())
242 .filter(|&ci| {
243 let (cmin, cmax) = m.chunk_aabb(ci);
244 rects_intersect(cmin, cmax, vmin, vmax)
245 })
246 .count();
247 assert_eq!(visible, 1);
248 }
249
250 #[test]
251 fn empty_tiles_emit_no_instances() {
252 let mut m = map();
253 m.set(3, 3, 5);
254 let insts = m.build_chunk(0);
255 assert_eq!(insts.len(), 1);
256 }
257}