1use glam::IVec3;
21use roxlap_formats::edit::{set_cube, set_rect, set_sphere};
22
23use crate::addr::{voxel_split, GridLocalPos};
24use crate::{Grid, CHUNK_SIZE_XY, CHUNK_SIZE_Z};
25
26#[inline]
30fn chunk_size_ivec3() -> IVec3 {
31 #[allow(clippy::cast_possible_wrap)]
32 IVec3::new(
33 CHUNK_SIZE_XY as i32,
34 CHUNK_SIZE_XY as i32,
35 CHUNK_SIZE_Z as i32,
36 )
37}
38
39impl Grid {
40 pub fn set_voxel(&mut self, voxel: IVec3, color: Option<u32>) {
50 self.billboards = None;
53 let (chunk_idx, in_chunk) = voxel_split(voxel);
54 if color.is_some() {
55 let vxl = self.ensure_chunk(chunk_idx);
56 #[allow(clippy::cast_possible_wrap)]
57 set_cube(
58 vxl,
59 in_chunk.x as i32,
60 in_chunk.y as i32,
61 in_chunk.z as i32,
62 color,
63 );
64 self.bump_chunk_version(chunk_idx);
66 } else if let Some(vxl) = self.chunks.get_mut(&chunk_idx) {
67 #[allow(clippy::cast_possible_wrap)]
68 set_cube(
69 vxl,
70 in_chunk.x as i32,
71 in_chunk.y as i32,
72 in_chunk.z as i32,
73 None,
74 );
75 self.bump_chunk_version(chunk_idx);
78 }
79 }
80
81 pub fn set_rect(&mut self, lo: IVec3, hi: IVec3, color: Option<u32>) {
93 self.billboards = None;
95 let lo_n = lo.min(hi);
96 let hi_n = lo.max(hi);
97 let (lo_c, _) = voxel_split(lo_n);
98 let (hi_c, _) = voxel_split(hi_n);
99 let cs = chunk_size_ivec3();
100
101 for cz in lo_c.z..=hi_c.z {
102 for cy in lo_c.y..=hi_c.y {
103 for cx in lo_c.x..=hi_c.x {
104 let chunk_idx = IVec3::new(cx, cy, cz);
105 let chunk_origin = chunk_idx * cs;
106 let chunk_end = chunk_origin + cs - IVec3::ONE;
107 let local_lo = lo_n.max(chunk_origin) - chunk_origin;
108 let local_hi = hi_n.min(chunk_end) - chunk_origin;
109 apply_set_rect(self, chunk_idx, local_lo, local_hi, color);
110 }
111 }
112 }
113 }
114
115 pub fn set_sphere(&mut self, centre: IVec3, radius: u32, color: Option<u32>) {
130 self.billboards = None;
132 #[allow(clippy::cast_possible_wrap)]
133 let r_i = radius as i32;
134 let lo = centre - IVec3::splat(r_i);
135 let hi = centre + IVec3::splat(r_i);
136 let (lo_c, _) = voxel_split(lo);
137 let (hi_c, _) = voxel_split(hi);
138 let cs = chunk_size_ivec3();
139
140 for cz in lo_c.z..=hi_c.z {
141 for cy in lo_c.y..=hi_c.y {
142 for cx in lo_c.x..=hi_c.x {
143 let chunk_idx = IVec3::new(cx, cy, cz);
144 let chunk_origin = chunk_idx * cs;
145 let local_centre = centre - chunk_origin;
146 apply_set_sphere(self, chunk_idx, local_centre, radius, color);
147 }
148 }
149 }
150 }
151}
152
153fn apply_set_rect(
154 grid: &mut Grid,
155 chunk_idx: IVec3,
156 local_lo: IVec3,
157 local_hi: IVec3,
158 color: Option<u32>,
159) {
160 let mut wrote = false;
161 if color.is_some() {
162 let vxl = grid.ensure_chunk(chunk_idx);
163 set_rect(vxl, local_lo.into(), local_hi.into(), color);
164 wrote = true;
165 } else if let Some(vxl) = grid.chunks.get_mut(&chunk_idx) {
166 set_rect(vxl, local_lo.into(), local_hi.into(), None);
167 wrote = true;
168 }
169 if wrote {
170 grid.bump_chunk_version(chunk_idx);
173 }
174}
175
176fn apply_set_sphere(
177 grid: &mut Grid,
178 chunk_idx: IVec3,
179 local_centre: IVec3,
180 radius: u32,
181 color: Option<u32>,
182) {
183 let mut wrote = false;
184 if color.is_some() {
185 let vxl = grid.ensure_chunk(chunk_idx);
186 set_sphere(vxl, local_centre.into(), radius, color);
187 wrote = true;
188 } else if let Some(vxl) = grid.chunks.get_mut(&chunk_idx) {
189 set_sphere(vxl, local_centre.into(), radius, None);
190 wrote = true;
191 }
192 if wrote {
193 grid.bump_chunk_version(chunk_idx);
195 }
196}
197
198#[must_use]
205pub fn voxel_at(local: &GridLocalPos) -> IVec3 {
206 crate::addr::voxel_global(local.chunk, local.voxel)
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::chunks::tests::voxel_is_solid;
213 use crate::GridTransform;
214
215 const TEST_COL: u32 = 0x80_aa_bb_cc;
216
217 #[test]
218 fn set_voxel_inserts_in_correct_chunk() {
219 let mut g = Grid::new(GridTransform::identity());
222 g.set_voxel(IVec3::new(5, 6, 7), Some(TEST_COL));
223 let vxl = g.chunk(IVec3::ZERO).expect("chunk created");
224 assert!(voxel_is_solid(vxl, 5, 6, 7));
225 assert!(!voxel_is_solid(vxl, 5, 6, 8));
227 assert_eq!(g.chunk_count(), 1);
228 }
229
230 #[test]
231 fn set_voxel_negative_coords_use_neg_chunk() {
232 let mut g = Grid::new(GridTransform::identity());
235 g.set_voxel(IVec3::new(-1, 0, 0), Some(TEST_COL));
236 assert!(g.chunk(IVec3::new(-1, 0, 0)).is_some());
237 let vxl = g.chunk(IVec3::new(-1, 0, 0)).unwrap();
238 assert!(voxel_is_solid(vxl, CHUNK_SIZE_XY - 1, 0, 0));
239 assert!(g.chunk(IVec3::ZERO).is_none());
241 }
242
243 #[test]
244 fn set_voxel_carve_then_insert_round_trips() {
245 let mut g = Grid::new(GridTransform::identity());
246 g.set_voxel(IVec3::new(10, 10, 10), Some(TEST_COL));
247 assert!(voxel_is_solid(g.chunk(IVec3::ZERO).unwrap(), 10, 10, 10));
248 g.set_voxel(IVec3::new(10, 10, 10), None);
249 assert!(!voxel_is_solid(g.chunk(IVec3::ZERO).unwrap(), 10, 10, 10));
250 }
251
252 #[test]
253 fn set_voxel_carve_in_missing_chunk_is_noop() {
254 let mut g = Grid::new(GridTransform::identity());
257 g.set_voxel(IVec3::new(5, 5, 5), None);
258 assert_eq!(g.chunk_count(), 0);
259 }
260
261 #[test]
262 fn set_rect_within_one_chunk() {
263 let mut g = Grid::new(GridTransform::identity());
264 g.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
265 assert_eq!(g.chunk_count(), 1);
266 let vxl = g.chunk(IVec3::ZERO).unwrap();
267 for z in 0..=3 {
268 for y in 0..=3 {
269 for x in 0..=3 {
270 assert!(voxel_is_solid(vxl, x, y, z), "({x},{y},{z}) air");
271 }
272 }
273 }
274 assert!(!voxel_is_solid(vxl, 4, 0, 0));
276 assert!(!voxel_is_solid(vxl, 0, 4, 0));
277 assert!(!voxel_is_solid(vxl, 0, 0, 4));
278 }
279
280 #[test]
281 fn set_rect_spans_two_chunks_x() {
282 let mut g = Grid::new(GridTransform::identity());
285 g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), Some(TEST_COL));
286 assert_eq!(g.chunk_count(), 2);
287
288 let v0 = g.chunk(IVec3::ZERO).unwrap();
290 assert!(voxel_is_solid(v0, 126, 0, 0));
291 assert!(voxel_is_solid(v0, 127, 0, 0));
292 assert!(!voxel_is_solid(v0, 125, 0, 0));
293
294 let v1 = g.chunk(IVec3::new(1, 0, 0)).unwrap();
296 assert!(voxel_is_solid(v1, 0, 0, 0));
297 assert!(voxel_is_solid(v1, 1, 0, 0));
298 assert!(!voxel_is_solid(v1, 2, 0, 0));
299 }
300
301 #[test]
302 fn set_rect_spans_z_boundary() {
303 let mut g = Grid::new(GridTransform::identity());
306 g.set_rect(IVec3::new(0, 0, 254), IVec3::new(0, 0, 257), Some(TEST_COL));
307 assert_eq!(g.chunk_count(), 2);
308 let v0 = g.chunk(IVec3::ZERO).unwrap();
309 assert!(voxel_is_solid(v0, 0, 0, 254));
310 assert!(voxel_is_solid(v0, 0, 0, 255));
311 let v1 = g.chunk(IVec3::new(0, 0, 1)).unwrap();
312 assert!(voxel_is_solid(v1, 0, 0, 0));
313 assert!(voxel_is_solid(v1, 0, 0, 1));
314 assert!(!voxel_is_solid(v1, 0, 0, 2));
315 }
316
317 #[test]
318 fn set_rect_unsorted_lo_hi_normalised() {
319 let mut g1 = Grid::new(GridTransform::identity());
321 let mut g2 = Grid::new(GridTransform::identity());
322 g1.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
323 g2.set_rect(IVec3::new(3, 3, 3), IVec3::new(0, 0, 0), Some(TEST_COL));
324 let v1 = g1.chunk(IVec3::ZERO).unwrap();
325 let v2 = g2.chunk(IVec3::ZERO).unwrap();
326 for z in 0..=3 {
327 for y in 0..=3 {
328 for x in 0..=3 {
329 assert_eq!(voxel_is_solid(v1, x, y, z), voxel_is_solid(v2, x, y, z));
330 }
331 }
332 }
333 }
334
335 #[test]
336 fn set_sphere_within_one_chunk() {
337 let mut g = Grid::new(GridTransform::identity());
338 g.set_sphere(IVec3::new(64, 64, 100), 5, Some(TEST_COL));
339 assert_eq!(g.chunk_count(), 1);
340 let vxl = g.chunk(IVec3::ZERO).unwrap();
341 assert!(voxel_is_solid(vxl, 64, 64, 100));
343 assert!(voxel_is_solid(vxl, 65, 64, 100));
345 assert!(voxel_is_solid(vxl, 64, 64, 105));
346 assert!(!voxel_is_solid(vxl, 70, 64, 100));
348 }
349
350 #[test]
351 fn set_sphere_spans_chunk_boundary() {
352 let mut g = Grid::new(GridTransform::identity());
355 g.set_sphere(IVec3::new(127, 64, 100), 4, Some(TEST_COL));
356 assert_eq!(g.chunk_count(), 2);
358
359 let v0 = g.chunk(IVec3::ZERO).unwrap();
360 assert!(voxel_is_solid(v0, 127, 64, 100));
363 assert!(voxel_is_solid(v0, 124, 64, 100));
365
366 let v1 = g.chunk(IVec3::new(1, 0, 0)).unwrap();
367 assert!(voxel_is_solid(v1, 0, 64, 100));
370 assert!(voxel_is_solid(v1, 2, 64, 100));
372 }
373
374 fn stamp_sentinel_cache(g: &mut Grid) {
382 g.billboards = Some(crate::BillboardCache::new_empty(32));
383 }
384
385 #[test]
386 fn set_voxel_invalidates_billboard_cache() {
387 let mut g = Grid::new(GridTransform::identity());
388 stamp_sentinel_cache(&mut g);
389 assert!(g.billboards.is_some());
390 g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
391 assert!(
392 g.billboards.is_none(),
393 "set_voxel should clear the billboard cache"
394 );
395 }
396
397 #[test]
398 fn set_voxel_carve_also_invalidates() {
399 let mut g = Grid::new(GridTransform::identity());
401 stamp_sentinel_cache(&mut g);
402 g.set_voxel(IVec3::new(5, 5, 5), None); assert!(
404 g.billboards.is_none(),
405 "carve should clear the cache (conservative)"
406 );
407 }
408
409 #[test]
410 fn set_rect_invalidates_billboard_cache() {
411 let mut g = Grid::new(GridTransform::identity());
412 stamp_sentinel_cache(&mut g);
413 g.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
414 assert!(g.billboards.is_none(), "set_rect should clear the cache");
415 }
416
417 #[test]
418 fn set_sphere_invalidates_billboard_cache() {
419 let mut g = Grid::new(GridTransform::identity());
420 stamp_sentinel_cache(&mut g);
421 g.set_sphere(IVec3::new(64, 64, 100), 5, Some(TEST_COL));
422 assert!(g.billboards.is_none(), "set_sphere should clear the cache");
423 }
424
425 #[test]
426 fn set_voxel_dispatches_to_correct_chunk_on_y_z_axes() {
427 let mut g = Grid::new(GridTransform::identity());
431 g.set_voxel(IVec3::new(200, 300, 500), Some(TEST_COL));
432 let vxl = g
433 .chunk(IVec3::new(1, 2, 1))
434 .expect("expected chunk (1, 2, 1)");
435 assert!(voxel_is_solid(vxl, 72, 44, 244));
436 }
437
438 #[test]
441 fn chunk_version_defaults_to_zero_for_missing() {
442 let g = Grid::new(GridTransform::identity());
443 assert_eq!(g.chunk_version(IVec3::ZERO), 0);
444 assert_eq!(g.chunk_version(IVec3::new(7, -3, 12)), 0);
445 }
446
447 #[test]
448 fn set_voxel_insert_bumps_to_one() {
449 let mut g = Grid::new(GridTransform::identity());
450 assert_eq!(g.chunk_version(IVec3::ZERO), 0);
451 g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
452 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
453 }
454
455 #[test]
456 fn set_voxel_carve_in_existing_chunk_bumps() {
457 let mut g = Grid::new(GridTransform::identity());
459 g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
460 g.set_voxel(IVec3::new(5, 5, 5), None);
461 assert_eq!(g.chunk_version(IVec3::ZERO), 2);
462 }
463
464 #[test]
465 fn set_voxel_carve_in_missing_chunk_does_not_bump() {
466 let mut g = Grid::new(GridTransform::identity());
468 g.set_voxel(IVec3::new(5, 5, 5), None);
469 assert_eq!(g.chunk_version(IVec3::ZERO), 0);
470 assert!(g.chunk_versions.is_empty());
471 }
472
473 #[test]
474 fn set_rect_multi_chunk_bumps_every_touched_chunk() {
475 let mut g = Grid::new(GridTransform::identity());
477 g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), Some(TEST_COL));
478 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
479 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
480 assert_eq!(g.chunk_versions.len(), 2);
482 }
483
484 #[test]
485 fn set_rect_carve_bumps_only_existing_chunks() {
486 let mut g = Grid::new(GridTransform::identity());
490 g.set_voxel(IVec3::new(0, 0, 0), Some(TEST_COL));
491 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
492 g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), None);
493 assert_eq!(g.chunk_version(IVec3::ZERO), 2);
494 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 0);
495 }
496
497 #[test]
498 fn set_sphere_multi_chunk_bumps_every_written_chunk() {
499 let mut g = Grid::new(GridTransform::identity());
502 g.set_sphere(IVec3::new(127, 64, 100), 4, Some(TEST_COL));
503 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
504 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
505 }
506}