1use glam::IVec3;
21use roxlap_formats::edit::{
22 set_cube, set_rect, set_rect_with_colfunc, set_sphere, set_sphere_with_colfunc,
23};
24
25use crate::addr::{voxel_split, GridLocalPos};
26use crate::{Grid, CHUNK_SIZE_XY, CHUNK_SIZE_Z};
27
28pub use roxlap_formats::edit::SpanOp;
33
34#[inline]
38fn chunk_size_ivec3() -> IVec3 {
39 #[allow(clippy::cast_possible_wrap)]
40 IVec3::new(
41 CHUNK_SIZE_XY as i32,
42 CHUNK_SIZE_XY as i32,
43 CHUNK_SIZE_Z as i32,
44 )
45}
46
47impl Grid {
48 pub fn set_voxel(&mut self, voxel: IVec3, color: Option<u32>) {
58 self.billboards = None;
61 let (chunk_idx, in_chunk) = voxel_split(voxel);
62 if color.is_some() {
63 let vxl = self.ensure_chunk(chunk_idx);
64 #[allow(clippy::cast_possible_wrap)]
65 set_cube(
66 vxl,
67 in_chunk.x as i32,
68 in_chunk.y as i32,
69 in_chunk.z as i32,
70 color,
71 );
72 self.bump_chunk_version(chunk_idx);
74 } else if let Some(vxl) = self.chunks.get_mut(&chunk_idx) {
75 #[allow(clippy::cast_possible_wrap)]
76 set_cube(
77 vxl,
78 in_chunk.x as i32,
79 in_chunk.y as i32,
80 in_chunk.z as i32,
81 None,
82 );
83 self.bump_chunk_version(chunk_idx);
86 }
87 }
88
89 pub fn set_rect(&mut self, lo: IVec3, hi: IVec3, color: Option<u32>) {
101 self.billboards = None;
103 let lo_n = lo.min(hi);
104 let hi_n = lo.max(hi);
105 let (lo_c, _) = voxel_split(lo_n);
106 let (hi_c, _) = voxel_split(hi_n);
107 let cs = chunk_size_ivec3();
108
109 for cz in lo_c.z..=hi_c.z {
110 for cy in lo_c.y..=hi_c.y {
111 for cx in lo_c.x..=hi_c.x {
112 let chunk_idx = IVec3::new(cx, cy, cz);
113 let chunk_origin = chunk_idx * cs;
114 let chunk_end = chunk_origin + cs - IVec3::ONE;
115 let local_lo = lo_n.max(chunk_origin) - chunk_origin;
116 let local_hi = hi_n.min(chunk_end) - chunk_origin;
117 apply_set_rect(self, chunk_idx, local_lo, local_hi, color);
118 }
119 }
120 }
121 }
122
123 pub fn set_sphere(&mut self, centre: IVec3, radius: u32, color: Option<u32>) {
138 self.billboards = None;
140 #[allow(clippy::cast_possible_wrap)]
141 let r_i = radius as i32;
142 let lo = centre - IVec3::splat(r_i);
143 let hi = centre + IVec3::splat(r_i);
144 let (lo_c, _) = voxel_split(lo);
145 let (hi_c, _) = voxel_split(hi);
146 let cs = chunk_size_ivec3();
147
148 for cz in lo_c.z..=hi_c.z {
149 for cy in lo_c.y..=hi_c.y {
150 for cx in lo_c.x..=hi_c.x {
151 let chunk_idx = IVec3::new(cx, cy, cz);
152 let chunk_origin = chunk_idx * cs;
153 let local_centre = centre - chunk_origin;
154 apply_set_sphere(self, chunk_idx, local_centre, radius, color);
155 }
156 }
157 }
158 }
159
160 pub fn set_sphere_with_colfunc<F>(
181 &mut self,
182 centre: IVec3,
183 radius: u32,
184 op: SpanOp,
185 mut colfunc: F,
186 ) where
187 F: FnMut(i32, i32, i32) -> i32,
188 {
189 self.billboards = None;
191 #[allow(clippy::cast_possible_wrap)]
192 let r_i = radius as i32;
193 let lo = centre - IVec3::splat(r_i);
194 let hi = centre + IVec3::splat(r_i);
195 let (lo_c, _) = voxel_split(lo);
196 let (hi_c, _) = voxel_split(hi);
197 let cs = chunk_size_ivec3();
198 let inserting = op == SpanOp::Insert;
199
200 for cz in lo_c.z..=hi_c.z {
201 for cy in lo_c.y..=hi_c.y {
202 for cx in lo_c.x..=hi_c.x {
203 let chunk_idx = IVec3::new(cx, cy, cz);
204 let chunk_origin = chunk_idx * cs;
205 let local_centre = centre - chunk_origin;
206 let (ox, oy, oz) = (chunk_origin.x, chunk_origin.y, chunk_origin.z);
207 let mut shim = |lx: i32, ly: i32, lz: i32| colfunc(lx + ox, ly + oy, lz + oz);
210 let mut wrote = false;
211 if inserting {
212 let vxl = self.ensure_chunk(chunk_idx);
213 set_sphere_with_colfunc(vxl, local_centre.into(), radius, op, &mut shim);
214 wrote = true;
215 } else if let Some(vxl) = self.chunks.get_mut(&chunk_idx) {
216 set_sphere_with_colfunc(vxl, local_centre.into(), radius, op, &mut shim);
217 wrote = true;
218 }
219 if wrote {
220 self.bump_chunk_version(chunk_idx);
221 }
222 }
223 }
224 }
225 }
226
227 pub fn set_rect_with_colfunc<F>(&mut self, lo: IVec3, hi: IVec3, op: SpanOp, mut colfunc: F)
235 where
236 F: FnMut(i32, i32, i32) -> i32,
237 {
238 self.billboards = None;
240 let lo_n = lo.min(hi);
241 let hi_n = lo.max(hi);
242 let (lo_c, _) = voxel_split(lo_n);
243 let (hi_c, _) = voxel_split(hi_n);
244 let cs = chunk_size_ivec3();
245 let inserting = op == SpanOp::Insert;
246
247 for cz in lo_c.z..=hi_c.z {
248 for cy in lo_c.y..=hi_c.y {
249 for cx in lo_c.x..=hi_c.x {
250 let chunk_idx = IVec3::new(cx, cy, cz);
251 let chunk_origin = chunk_idx * cs;
252 let chunk_end = chunk_origin + cs - IVec3::ONE;
253 let local_lo = lo_n.max(chunk_origin) - chunk_origin;
254 let local_hi = hi_n.min(chunk_end) - chunk_origin;
255 let (ox, oy, oz) = (chunk_origin.x, chunk_origin.y, chunk_origin.z);
256 let mut shim = |lx: i32, ly: i32, lz: i32| colfunc(lx + ox, ly + oy, lz + oz);
257 let mut wrote = false;
258 if inserting {
259 let vxl = self.ensure_chunk(chunk_idx);
260 set_rect_with_colfunc(vxl, local_lo.into(), local_hi.into(), op, &mut shim);
261 wrote = true;
262 } else if let Some(vxl) = self.chunks.get_mut(&chunk_idx) {
263 set_rect_with_colfunc(vxl, local_lo.into(), local_hi.into(), op, &mut shim);
264 wrote = true;
265 }
266 if wrote {
267 self.bump_chunk_version(chunk_idx);
268 }
269 }
270 }
271 }
272 }
273}
274
275fn apply_set_rect(
276 grid: &mut Grid,
277 chunk_idx: IVec3,
278 local_lo: IVec3,
279 local_hi: IVec3,
280 color: Option<u32>,
281) {
282 let mut wrote = false;
283 if color.is_some() {
284 let vxl = grid.ensure_chunk(chunk_idx);
285 set_rect(vxl, local_lo.into(), local_hi.into(), color);
286 wrote = true;
287 } else if let Some(vxl) = grid.chunks.get_mut(&chunk_idx) {
288 set_rect(vxl, local_lo.into(), local_hi.into(), None);
289 wrote = true;
290 }
291 if wrote {
292 grid.bump_chunk_version(chunk_idx);
295 }
296}
297
298fn apply_set_sphere(
299 grid: &mut Grid,
300 chunk_idx: IVec3,
301 local_centre: IVec3,
302 radius: u32,
303 color: Option<u32>,
304) {
305 let mut wrote = false;
306 if color.is_some() {
307 let vxl = grid.ensure_chunk(chunk_idx);
308 set_sphere(vxl, local_centre.into(), radius, color);
309 wrote = true;
310 } else if let Some(vxl) = grid.chunks.get_mut(&chunk_idx) {
311 set_sphere(vxl, local_centre.into(), radius, None);
312 wrote = true;
313 }
314 if wrote {
315 grid.bump_chunk_version(chunk_idx);
317 }
318}
319
320#[must_use]
327pub fn voxel_at(local: &GridLocalPos) -> IVec3 {
328 crate::addr::voxel_global(local.chunk, local.voxel)
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::chunks::tests::voxel_is_solid;
335 use crate::GridTransform;
336
337 const TEST_COL: u32 = 0x80_aa_bb_cc;
338
339 #[test]
340 fn set_voxel_inserts_in_correct_chunk() {
341 let mut g = Grid::new(GridTransform::identity());
344 g.set_voxel(IVec3::new(5, 6, 7), Some(TEST_COL));
345 let vxl = g.chunk(IVec3::ZERO).expect("chunk created");
346 assert!(voxel_is_solid(vxl, 5, 6, 7));
347 assert!(!voxel_is_solid(vxl, 5, 6, 8));
349 assert_eq!(g.chunk_count(), 1);
350 }
351
352 #[test]
353 fn set_voxel_negative_coords_use_neg_chunk() {
354 let mut g = Grid::new(GridTransform::identity());
357 g.set_voxel(IVec3::new(-1, 0, 0), Some(TEST_COL));
358 assert!(g.chunk(IVec3::new(-1, 0, 0)).is_some());
359 let vxl = g.chunk(IVec3::new(-1, 0, 0)).unwrap();
360 assert!(voxel_is_solid(vxl, CHUNK_SIZE_XY - 1, 0, 0));
361 assert!(g.chunk(IVec3::ZERO).is_none());
363 }
364
365 #[test]
366 fn set_voxel_carve_then_insert_round_trips() {
367 let mut g = Grid::new(GridTransform::identity());
368 g.set_voxel(IVec3::new(10, 10, 10), Some(TEST_COL));
369 assert!(voxel_is_solid(g.chunk(IVec3::ZERO).unwrap(), 10, 10, 10));
370 g.set_voxel(IVec3::new(10, 10, 10), None);
371 assert!(!voxel_is_solid(g.chunk(IVec3::ZERO).unwrap(), 10, 10, 10));
372 }
373
374 #[test]
375 fn set_voxel_carve_in_missing_chunk_is_noop() {
376 let mut g = Grid::new(GridTransform::identity());
379 g.set_voxel(IVec3::new(5, 5, 5), None);
380 assert_eq!(g.chunk_count(), 0);
381 }
382
383 #[test]
384 fn set_rect_within_one_chunk() {
385 let mut g = Grid::new(GridTransform::identity());
386 g.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
387 assert_eq!(g.chunk_count(), 1);
388 let vxl = g.chunk(IVec3::ZERO).unwrap();
389 for z in 0..=3 {
390 for y in 0..=3 {
391 for x in 0..=3 {
392 assert!(voxel_is_solid(vxl, x, y, z), "({x},{y},{z}) air");
393 }
394 }
395 }
396 assert!(!voxel_is_solid(vxl, 4, 0, 0));
398 assert!(!voxel_is_solid(vxl, 0, 4, 0));
399 assert!(!voxel_is_solid(vxl, 0, 0, 4));
400 }
401
402 #[test]
403 fn set_rect_spans_two_chunks_x() {
404 let mut g = Grid::new(GridTransform::identity());
407 g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), Some(TEST_COL));
408 assert_eq!(g.chunk_count(), 2);
409
410 let v0 = g.chunk(IVec3::ZERO).unwrap();
412 assert!(voxel_is_solid(v0, 126, 0, 0));
413 assert!(voxel_is_solid(v0, 127, 0, 0));
414 assert!(!voxel_is_solid(v0, 125, 0, 0));
415
416 let v1 = g.chunk(IVec3::new(1, 0, 0)).unwrap();
418 assert!(voxel_is_solid(v1, 0, 0, 0));
419 assert!(voxel_is_solid(v1, 1, 0, 0));
420 assert!(!voxel_is_solid(v1, 2, 0, 0));
421 }
422
423 #[test]
424 fn set_rect_spans_z_boundary() {
425 let mut g = Grid::new(GridTransform::identity());
428 g.set_rect(IVec3::new(0, 0, 254), IVec3::new(0, 0, 257), Some(TEST_COL));
429 assert_eq!(g.chunk_count(), 2);
430 let v0 = g.chunk(IVec3::ZERO).unwrap();
431 assert!(voxel_is_solid(v0, 0, 0, 254));
432 assert!(voxel_is_solid(v0, 0, 0, 255));
433 let v1 = g.chunk(IVec3::new(0, 0, 1)).unwrap();
434 assert!(voxel_is_solid(v1, 0, 0, 0));
435 assert!(voxel_is_solid(v1, 0, 0, 1));
436 assert!(!voxel_is_solid(v1, 0, 0, 2));
437 }
438
439 #[test]
440 fn set_rect_unsorted_lo_hi_normalised() {
441 let mut g1 = Grid::new(GridTransform::identity());
443 let mut g2 = Grid::new(GridTransform::identity());
444 g1.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
445 g2.set_rect(IVec3::new(3, 3, 3), IVec3::new(0, 0, 0), Some(TEST_COL));
446 let v1 = g1.chunk(IVec3::ZERO).unwrap();
447 let v2 = g2.chunk(IVec3::ZERO).unwrap();
448 for z in 0..=3 {
449 for y in 0..=3 {
450 for x in 0..=3 {
451 assert_eq!(voxel_is_solid(v1, x, y, z), voxel_is_solid(v2, x, y, z));
452 }
453 }
454 }
455 }
456
457 #[test]
458 fn set_sphere_within_one_chunk() {
459 let mut g = Grid::new(GridTransform::identity());
460 g.set_sphere(IVec3::new(64, 64, 100), 5, Some(TEST_COL));
461 assert_eq!(g.chunk_count(), 1);
462 let vxl = g.chunk(IVec3::ZERO).unwrap();
463 assert!(voxel_is_solid(vxl, 64, 64, 100));
465 assert!(voxel_is_solid(vxl, 65, 64, 100));
467 assert!(voxel_is_solid(vxl, 64, 64, 105));
468 assert!(!voxel_is_solid(vxl, 70, 64, 100));
470 }
471
472 #[test]
473 fn set_sphere_spans_chunk_boundary() {
474 let mut g = Grid::new(GridTransform::identity());
477 g.set_sphere(IVec3::new(127, 64, 100), 4, Some(TEST_COL));
478 assert_eq!(g.chunk_count(), 2);
480
481 let v0 = g.chunk(IVec3::ZERO).unwrap();
482 assert!(voxel_is_solid(v0, 127, 64, 100));
485 assert!(voxel_is_solid(v0, 124, 64, 100));
487
488 let v1 = g.chunk(IVec3::new(1, 0, 0)).unwrap();
489 assert!(voxel_is_solid(v1, 0, 64, 100));
492 assert!(voxel_is_solid(v1, 2, 64, 100));
494 }
495
496 fn stamp_sentinel_cache(g: &mut Grid) {
504 g.billboards = Some(crate::BillboardCache::new_empty(32));
505 }
506
507 #[test]
508 fn set_voxel_invalidates_billboard_cache() {
509 let mut g = Grid::new(GridTransform::identity());
510 stamp_sentinel_cache(&mut g);
511 assert!(g.billboards.is_some());
512 g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
513 assert!(
514 g.billboards.is_none(),
515 "set_voxel should clear the billboard cache"
516 );
517 }
518
519 #[test]
520 fn set_voxel_carve_also_invalidates() {
521 let mut g = Grid::new(GridTransform::identity());
523 stamp_sentinel_cache(&mut g);
524 g.set_voxel(IVec3::new(5, 5, 5), None); assert!(
526 g.billboards.is_none(),
527 "carve should clear the cache (conservative)"
528 );
529 }
530
531 #[test]
532 fn set_rect_invalidates_billboard_cache() {
533 let mut g = Grid::new(GridTransform::identity());
534 stamp_sentinel_cache(&mut g);
535 g.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
536 assert!(g.billboards.is_none(), "set_rect should clear the cache");
537 }
538
539 #[test]
540 fn set_sphere_invalidates_billboard_cache() {
541 let mut g = Grid::new(GridTransform::identity());
542 stamp_sentinel_cache(&mut g);
543 g.set_sphere(IVec3::new(64, 64, 100), 5, Some(TEST_COL));
544 assert!(g.billboards.is_none(), "set_sphere should clear the cache");
545 }
546
547 #[test]
548 fn set_voxel_dispatches_to_correct_chunk_on_y_z_axes() {
549 let mut g = Grid::new(GridTransform::identity());
553 g.set_voxel(IVec3::new(200, 300, 500), Some(TEST_COL));
554 let vxl = g
555 .chunk(IVec3::new(1, 2, 1))
556 .expect("expected chunk (1, 2, 1)");
557 assert!(voxel_is_solid(vxl, 72, 44, 244));
558 }
559
560 #[test]
563 fn chunk_version_defaults_to_zero_for_missing() {
564 let g = Grid::new(GridTransform::identity());
565 assert_eq!(g.chunk_version(IVec3::ZERO), 0);
566 assert_eq!(g.chunk_version(IVec3::new(7, -3, 12)), 0);
567 }
568
569 #[test]
570 fn set_voxel_insert_bumps_to_one() {
571 let mut g = Grid::new(GridTransform::identity());
572 assert_eq!(g.chunk_version(IVec3::ZERO), 0);
573 g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
574 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
575 }
576
577 #[test]
578 fn set_voxel_carve_in_existing_chunk_bumps() {
579 let mut g = Grid::new(GridTransform::identity());
581 g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
582 g.set_voxel(IVec3::new(5, 5, 5), None);
583 assert_eq!(g.chunk_version(IVec3::ZERO), 2);
584 }
585
586 #[test]
587 fn set_voxel_carve_in_missing_chunk_does_not_bump() {
588 let mut g = Grid::new(GridTransform::identity());
590 g.set_voxel(IVec3::new(5, 5, 5), None);
591 assert_eq!(g.chunk_version(IVec3::ZERO), 0);
592 assert!(g.chunk_versions.is_empty());
593 }
594
595 #[test]
596 fn set_rect_multi_chunk_bumps_every_touched_chunk() {
597 let mut g = Grid::new(GridTransform::identity());
599 g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), Some(TEST_COL));
600 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
601 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
602 assert_eq!(g.chunk_versions.len(), 2);
604 }
605
606 #[test]
607 fn set_rect_carve_bumps_only_existing_chunks() {
608 let mut g = Grid::new(GridTransform::identity());
612 g.set_voxel(IVec3::new(0, 0, 0), Some(TEST_COL));
613 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
614 g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), None);
615 assert_eq!(g.chunk_version(IVec3::ZERO), 2);
616 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 0);
617 }
618
619 #[test]
626 fn set_sphere_with_colfunc_paints_exposed_interior() {
627 const CRATER: i32 = 0x00_44_55_66;
628 let mut g = Grid::new(GridTransform::identity());
630 g.set_rect(
631 IVec3::new(40, 40, 40),
632 IVec3::new(90, 90, 90),
633 Some(TEST_COL),
634 );
635 assert!(g.voxel_color(IVec3::new(64, 64, 55)).is_none()); g.set_sphere_with_colfunc(IVec3::new(64, 64, 64), 8, SpanOp::Carve, |_x, _y, _z| {
638 CRATER
639 });
640
641 assert!(!g.voxel_solid(IVec3::new(64, 64, 64)));
643 assert!(g.voxel_solid(IVec3::new(64, 64, 55)));
646 assert_eq!(g.voxel_color(IVec3::new(64, 64, 55)), Some(CRATER as u32));
647
648 let mut g2 = Grid::new(GridTransform::identity());
651 g2.set_rect(
652 IVec3::new(40, 40, 40),
653 IVec3::new(90, 90, 90),
654 Some(TEST_COL),
655 );
656 g2.set_sphere(IVec3::new(64, 64, 64), 8, None);
657 assert!(g2.voxel_solid(IVec3::new(64, 64, 55)));
658 assert_eq!(g2.voxel_color(IVec3::new(64, 64, 55)), None);
659 }
660
661 #[test]
667 fn set_sphere_with_colfunc_uses_grid_local_coords_across_chunks() {
668 #[allow(clippy::cast_sign_loss)]
670 let encode = |x: i32, y: i32, z: i32| (x << 16) | (y << 8) | z;
671
672 let mut g = Grid::new(GridTransform::identity());
673 g.set_rect(
675 IVec3::new(120, 60, 60),
676 IVec3::new(140, 80, 80),
677 Some(TEST_COL),
678 );
679 g.set_sphere_with_colfunc(IVec3::new(128, 70, 70), 5, SpanOp::Carve, |x, y, z| {
681 encode(x, y, z)
682 });
683
684 let p = IVec3::new(130, 70, 65);
689 assert!(g.voxel_solid(p));
690 #[allow(clippy::cast_sign_loss)]
691 let want = encode(130, 70, 65) as u32;
692 assert_eq!(g.voxel_color(p), Some(want));
693 #[allow(clippy::cast_sign_loss)]
695 let chunk_local = encode(2, 70, 65) as u32;
696 assert_ne!(g.voxel_color(p), Some(chunk_local));
697 }
698
699 #[test]
700 fn set_rect_with_colfunc_carve_paints_exposed_face() {
701 const WALL: i32 = 0x00_12_34_56;
702 let mut g = Grid::new(GridTransform::identity());
703 g.set_rect(
704 IVec3::new(40, 40, 40),
705 IVec3::new(90, 90, 90),
706 Some(TEST_COL),
707 );
708 g.set_rect_with_colfunc(
710 IVec3::new(50, 50, 50),
711 IVec3::new(80, 80, 80),
712 SpanOp::Carve,
713 |_x, _y, _z| WALL,
714 );
715 assert!(!g.voxel_solid(IVec3::new(64, 64, 64)));
716 assert!(g.voxel_solid(IVec3::new(64, 64, 49)));
717 assert_eq!(g.voxel_color(IVec3::new(64, 64, 49)), Some(WALL as u32));
718 }
719
720 #[test]
721 fn set_sphere_with_colfunc_invalidates_billboard_cache() {
722 let mut g = Grid::new(GridTransform::identity());
723 g.set_rect(
724 IVec3::new(40, 40, 40),
725 IVec3::new(90, 90, 90),
726 Some(TEST_COL),
727 );
728 stamp_sentinel_cache(&mut g);
729 g.set_sphere_with_colfunc(IVec3::new(64, 64, 64), 6, SpanOp::Carve, |_, _, _| 1);
730 assert!(g.billboards.is_none());
731 }
732
733 #[test]
734 fn set_sphere_multi_chunk_bumps_every_written_chunk() {
735 let mut g = Grid::new(GridTransform::identity());
738 g.set_sphere(IVec3::new(127, 64, 100), 4, Some(TEST_COL));
739 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
740 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
741 }
742}