1use glam::IVec3;
21use roxlap_formats::color::VoxColor;
22use roxlap_formats::edit::{
23 set_cube, set_rect, set_rect_with_colfunc, set_sphere, set_sphere_with_colfunc,
24};
25
26use crate::addr::{voxel_split, GridLocalPos};
27use crate::{Grid, CHUNK_SIZE_XY, CHUNK_SIZE_Z};
28
29pub use roxlap_formats::edit::SpanOp;
34
35#[inline]
39fn chunk_size_ivec3() -> IVec3 {
40 #[allow(clippy::cast_possible_wrap)]
41 IVec3::new(
42 CHUNK_SIZE_XY as i32,
43 CHUNK_SIZE_XY as i32,
44 CHUNK_SIZE_Z as i32,
45 )
46}
47
48impl Grid {
49 pub fn set_voxel(&mut self, voxel: IVec3, color: Option<VoxColor>) {
59 self.billboards = None;
62 let (chunk_idx, in_chunk) = voxel_split(voxel);
63 if color.is_some() {
64 let vxl = self.ensure_chunk(chunk_idx);
65 #[allow(clippy::cast_possible_wrap)]
66 set_cube(
67 vxl,
68 in_chunk.x as i32,
69 in_chunk.y as i32,
70 in_chunk.z as i32,
71 color,
72 );
73 let (lo, hi) = dirty_pad(in_chunk.as_ivec3(), in_chunk.as_ivec3());
77 self.bump_chunk_version_bbox(chunk_idx, lo, hi);
78 } else if let Some(vxl) = self.chunks.get_mut(&chunk_idx) {
79 #[allow(clippy::cast_possible_wrap)]
80 set_cube(
81 vxl,
82 in_chunk.x as i32,
83 in_chunk.y as i32,
84 in_chunk.z as i32,
85 None,
86 );
87 let (lo, hi) = dirty_pad(in_chunk.as_ivec3(), in_chunk.as_ivec3());
90 self.bump_chunk_version_bbox(chunk_idx, lo, hi);
91 }
92 }
93
94 pub fn set_rect(&mut self, lo: IVec3, hi: IVec3, color: Option<VoxColor>) {
106 self.billboards = None;
108 let lo_n = lo.min(hi);
109 let hi_n = lo.max(hi);
110 let (lo_c, _) = voxel_split(lo_n);
111 let (hi_c, _) = voxel_split(hi_n);
112 let cs = chunk_size_ivec3();
113
114 for cz in lo_c.z..=hi_c.z {
115 for cy in lo_c.y..=hi_c.y {
116 for cx in lo_c.x..=hi_c.x {
117 let chunk_idx = IVec3::new(cx, cy, cz);
118 let chunk_origin = chunk_idx * cs;
119 let chunk_end = chunk_origin + cs - IVec3::ONE;
120 let local_lo = lo_n.max(chunk_origin) - chunk_origin;
121 let local_hi = hi_n.min(chunk_end) - chunk_origin;
122 apply_set_rect(self, chunk_idx, local_lo, local_hi, color);
123 }
124 }
125 }
126 }
127
128 pub fn set_sphere(&mut self, centre: IVec3, radius: u32, color: Option<VoxColor>) {
143 self.billboards = None;
145 #[allow(clippy::cast_possible_wrap)]
146 let r_i = radius as i32;
147 let lo = centre - IVec3::splat(r_i);
148 let hi = centre + IVec3::splat(r_i);
149 let (lo_c, _) = voxel_split(lo);
150 let (hi_c, _) = voxel_split(hi);
151 let cs = chunk_size_ivec3();
152
153 for cz in lo_c.z..=hi_c.z {
154 for cy in lo_c.y..=hi_c.y {
155 for cx in lo_c.x..=hi_c.x {
156 let chunk_idx = IVec3::new(cx, cy, cz);
157 let chunk_origin = chunk_idx * cs;
158 let local_centre = centre - chunk_origin;
159 apply_set_sphere(self, chunk_idx, local_centre, radius, color);
160 }
161 }
162 }
163 }
164
165 pub fn set_sphere_with_colfunc<F>(
186 &mut self,
187 centre: IVec3,
188 radius: u32,
189 op: SpanOp,
190 mut colfunc: F,
191 ) where
192 F: FnMut(i32, i32, i32) -> VoxColor,
193 {
194 self.billboards = None;
196 #[allow(clippy::cast_possible_wrap)]
197 let r_i = radius as i32;
198 let lo = centre - IVec3::splat(r_i);
199 let hi = centre + IVec3::splat(r_i);
200 let (lo_c, _) = voxel_split(lo);
201 let (hi_c, _) = voxel_split(hi);
202 let cs = chunk_size_ivec3();
203 let inserting = op == SpanOp::Insert;
204
205 for cz in lo_c.z..=hi_c.z {
206 for cy in lo_c.y..=hi_c.y {
207 for cx in lo_c.x..=hi_c.x {
208 let chunk_idx = IVec3::new(cx, cy, cz);
209 let chunk_origin = chunk_idx * cs;
210 let local_centre = centre - chunk_origin;
211 let (ox, oy, oz) = (chunk_origin.x, chunk_origin.y, chunk_origin.z);
212 let mut shim = |lx: i32, ly: i32, lz: i32| colfunc(lx + ox, ly + oy, lz + oz);
215 let mut wrote = false;
216 if inserting {
217 let vxl = self.ensure_chunk(chunk_idx);
218 set_sphere_with_colfunc(vxl, local_centre.into(), radius, op, &mut shim);
219 wrote = true;
220 } else if let Some(vxl) = self.chunks.get_mut(&chunk_idx) {
221 set_sphere_with_colfunc(vxl, local_centre.into(), radius, op, &mut shim);
222 wrote = true;
223 }
224 if wrote {
225 self.bump_chunk_version(chunk_idx);
226 }
227 }
228 }
229 }
230 }
231
232 pub fn set_rect_with_colfunc<F>(&mut self, lo: IVec3, hi: IVec3, op: SpanOp, mut colfunc: F)
240 where
241 F: FnMut(i32, i32, i32) -> VoxColor,
242 {
243 self.billboards = None;
245 let lo_n = lo.min(hi);
246 let hi_n = lo.max(hi);
247 let (lo_c, _) = voxel_split(lo_n);
248 let (hi_c, _) = voxel_split(hi_n);
249 let cs = chunk_size_ivec3();
250 let inserting = op == SpanOp::Insert;
251
252 for cz in lo_c.z..=hi_c.z {
253 for cy in lo_c.y..=hi_c.y {
254 for cx in lo_c.x..=hi_c.x {
255 let chunk_idx = IVec3::new(cx, cy, cz);
256 let chunk_origin = chunk_idx * cs;
257 let chunk_end = chunk_origin + cs - IVec3::ONE;
258 let local_lo = lo_n.max(chunk_origin) - chunk_origin;
259 let local_hi = hi_n.min(chunk_end) - chunk_origin;
260 let (ox, oy, oz) = (chunk_origin.x, chunk_origin.y, chunk_origin.z);
261 let mut shim = |lx: i32, ly: i32, lz: i32| colfunc(lx + ox, ly + oy, lz + oz);
262 let mut wrote = false;
263 if inserting {
264 let vxl = self.ensure_chunk(chunk_idx);
265 set_rect_with_colfunc(vxl, local_lo.into(), local_hi.into(), op, &mut shim);
266 wrote = true;
267 } else if let Some(vxl) = self.chunks.get_mut(&chunk_idx) {
268 set_rect_with_colfunc(vxl, local_lo.into(), local_hi.into(), op, &mut shim);
269 wrote = true;
270 }
271 if wrote {
272 self.bump_chunk_version(chunk_idx);
273 }
274 }
275 }
276 }
277 }
278}
279
280fn apply_set_rect(
281 grid: &mut Grid,
282 chunk_idx: IVec3,
283 local_lo: IVec3,
284 local_hi: IVec3,
285 color: Option<VoxColor>,
286) {
287 let mut wrote = false;
288 if color.is_some() {
289 let vxl = grid.ensure_chunk(chunk_idx);
290 set_rect(vxl, local_lo.into(), local_hi.into(), color);
291 wrote = true;
292 } else if let Some(vxl) = grid.chunks.get_mut(&chunk_idx) {
293 set_rect(vxl, local_lo.into(), local_hi.into(), None);
294 wrote = true;
295 }
296 if wrote {
297 let (lo, hi) = dirty_pad(local_lo, local_hi);
300 grid.bump_chunk_version_bbox(chunk_idx, lo, hi);
301 }
302}
303
304fn apply_set_sphere(
305 grid: &mut Grid,
306 chunk_idx: IVec3,
307 local_centre: IVec3,
308 radius: u32,
309 color: Option<VoxColor>,
310) {
311 let mut wrote = false;
312 if color.is_some() {
313 let vxl = grid.ensure_chunk(chunk_idx);
314 set_sphere(vxl, local_centre.into(), radius, color);
315 wrote = true;
316 } else if let Some(vxl) = grid.chunks.get_mut(&chunk_idx) {
317 set_sphere(vxl, local_centre.into(), radius, None);
318 wrote = true;
319 }
320 if wrote {
321 #[allow(clippy::cast_possible_wrap)]
324 let r = radius as i32;
325 let (lo, hi) = dirty_pad(
326 local_centre - IVec3::splat(r),
327 local_centre + IVec3::splat(r),
328 );
329 grid.bump_chunk_version_bbox(chunk_idx, lo, hi);
330 }
331}
332
333fn dirty_pad(lo: IVec3, hi: IVec3) -> (IVec3, IVec3) {
337 let cs = chunk_size_ivec3();
338 (
339 (lo - IVec3::ONE).max(IVec3::ZERO),
340 (hi + IVec3::ONE).min(cs - IVec3::ONE),
341 )
342}
343
344#[must_use]
351pub fn voxel_at(local: &GridLocalPos) -> IVec3 {
352 crate::addr::voxel_global(local.chunk, local.voxel)
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use crate::chunks::tests::voxel_is_solid;
359 use crate::GridTransform;
360
361 const TEST_COL: VoxColor = VoxColor(0x80_aa_bb_cc);
362
363 #[test]
364 fn set_voxel_inserts_in_correct_chunk() {
365 let mut g = Grid::new(GridTransform::identity());
368 g.set_voxel(IVec3::new(5, 6, 7), Some(TEST_COL));
369 let vxl = g.chunk(IVec3::ZERO).expect("chunk created");
370 assert!(voxel_is_solid(vxl, 5, 6, 7));
371 assert!(!voxel_is_solid(vxl, 5, 6, 8));
373 assert_eq!(g.chunk_count(), 1);
374 }
375
376 #[test]
377 fn set_voxel_negative_coords_use_neg_chunk() {
378 let mut g = Grid::new(GridTransform::identity());
381 g.set_voxel(IVec3::new(-1, 0, 0), Some(TEST_COL));
382 assert!(g.chunk(IVec3::new(-1, 0, 0)).is_some());
383 let vxl = g.chunk(IVec3::new(-1, 0, 0)).unwrap();
384 assert!(voxel_is_solid(vxl, CHUNK_SIZE_XY - 1, 0, 0));
385 assert!(g.chunk(IVec3::ZERO).is_none());
387 }
388
389 #[test]
390 fn set_voxel_carve_then_insert_round_trips() {
391 let mut g = Grid::new(GridTransform::identity());
392 g.set_voxel(IVec3::new(10, 10, 10), Some(TEST_COL));
393 assert!(voxel_is_solid(g.chunk(IVec3::ZERO).unwrap(), 10, 10, 10));
394 g.set_voxel(IVec3::new(10, 10, 10), None);
395 assert!(!voxel_is_solid(g.chunk(IVec3::ZERO).unwrap(), 10, 10, 10));
396 }
397
398 #[test]
399 fn set_voxel_carve_in_missing_chunk_is_noop() {
400 let mut g = Grid::new(GridTransform::identity());
403 g.set_voxel(IVec3::new(5, 5, 5), None);
404 assert_eq!(g.chunk_count(), 0);
405 }
406
407 #[test]
414 fn ao_only_concave_on_setrect_pillar() {
415 let mut g = Grid::new(GridTransform::identity());
416 g.set_rect(
417 IVec3::new(0, 0, 60),
418 IVec3::new(64, 64, 63),
419 Some(VoxColor(0x80_4d_8a_3a)),
420 ); g.set_rect(
422 IVec3::new(20, 20, 30),
423 IVec3::new(30, 30, 60),
424 Some(VoxColor(0x80_8a_8a_92)),
425 ); let vxl = g.chunk(IVec3::ZERO).expect("chunk");
427 let cache = roxlap_core::EstNormCache::build(
428 &vxl.data,
429 &vxl.column_offset,
430 CHUNK_SIZE_XY,
431 16,
432 16,
433 40,
434 40,
435 );
436 let ao = |x, y, z| cache.ambient_occlusion(x, y, z, 1);
437
438 for x in 20..30 {
441 for y in 20..30 {
442 assert!(
443 ao(x, y, 30) < 0.01,
444 "convex top ({x},{y},30) occluded: {}",
445 ao(x, y, 30)
446 );
447 }
448 for z in 31..57 {
449 let a = ao(x, 20, z);
450 assert!(
451 a < 0.01,
452 "flat front face ({x},20,{z}) occluded (pillow): {a}"
453 );
454 }
455 }
456 assert!(
458 ao(19, 24, 60) > 0.1,
459 "concave base must occlude: {}",
460 ao(19, 24, 60)
461 );
462 }
463
464 #[test]
465 fn set_rect_within_one_chunk() {
466 let mut g = Grid::new(GridTransform::identity());
467 g.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
468 assert_eq!(g.chunk_count(), 1);
469 let vxl = g.chunk(IVec3::ZERO).unwrap();
470 for z in 0..=3 {
471 for y in 0..=3 {
472 for x in 0..=3 {
473 assert!(voxel_is_solid(vxl, x, y, z), "({x},{y},{z}) air");
474 }
475 }
476 }
477 assert!(!voxel_is_solid(vxl, 4, 0, 0));
479 assert!(!voxel_is_solid(vxl, 0, 4, 0));
480 assert!(!voxel_is_solid(vxl, 0, 0, 4));
481 }
482
483 #[test]
484 fn set_rect_spans_two_chunks_x() {
485 let mut g = Grid::new(GridTransform::identity());
488 g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), Some(TEST_COL));
489 assert_eq!(g.chunk_count(), 2);
490
491 let v0 = g.chunk(IVec3::ZERO).unwrap();
493 assert!(voxel_is_solid(v0, 126, 0, 0));
494 assert!(voxel_is_solid(v0, 127, 0, 0));
495 assert!(!voxel_is_solid(v0, 125, 0, 0));
496
497 let v1 = g.chunk(IVec3::new(1, 0, 0)).unwrap();
499 assert!(voxel_is_solid(v1, 0, 0, 0));
500 assert!(voxel_is_solid(v1, 1, 0, 0));
501 assert!(!voxel_is_solid(v1, 2, 0, 0));
502 }
503
504 #[test]
505 fn set_rect_spans_z_boundary() {
506 let mut g = Grid::new(GridTransform::identity());
509 g.set_rect(IVec3::new(0, 0, 254), IVec3::new(0, 0, 257), Some(TEST_COL));
510 assert_eq!(g.chunk_count(), 2);
511 let v0 = g.chunk(IVec3::ZERO).unwrap();
512 assert!(voxel_is_solid(v0, 0, 0, 254));
513 assert!(voxel_is_solid(v0, 0, 0, 255));
514 let v1 = g.chunk(IVec3::new(0, 0, 1)).unwrap();
515 assert!(voxel_is_solid(v1, 0, 0, 0));
516 assert!(voxel_is_solid(v1, 0, 0, 1));
517 assert!(!voxel_is_solid(v1, 0, 0, 2));
518 }
519
520 #[test]
521 fn set_rect_unsorted_lo_hi_normalised() {
522 let mut g1 = Grid::new(GridTransform::identity());
524 let mut g2 = Grid::new(GridTransform::identity());
525 g1.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
526 g2.set_rect(IVec3::new(3, 3, 3), IVec3::new(0, 0, 0), Some(TEST_COL));
527 let v1 = g1.chunk(IVec3::ZERO).unwrap();
528 let v2 = g2.chunk(IVec3::ZERO).unwrap();
529 for z in 0..=3 {
530 for y in 0..=3 {
531 for x in 0..=3 {
532 assert_eq!(voxel_is_solid(v1, x, y, z), voxel_is_solid(v2, x, y, z));
533 }
534 }
535 }
536 }
537
538 #[test]
539 fn set_sphere_within_one_chunk() {
540 let mut g = Grid::new(GridTransform::identity());
541 g.set_sphere(IVec3::new(64, 64, 100), 5, Some(TEST_COL));
542 assert_eq!(g.chunk_count(), 1);
543 let vxl = g.chunk(IVec3::ZERO).unwrap();
544 assert!(voxel_is_solid(vxl, 64, 64, 100));
546 assert!(voxel_is_solid(vxl, 65, 64, 100));
548 assert!(voxel_is_solid(vxl, 64, 64, 105));
549 assert!(!voxel_is_solid(vxl, 70, 64, 100));
551 }
552
553 #[test]
554 fn set_sphere_spans_chunk_boundary() {
555 let mut g = Grid::new(GridTransform::identity());
558 g.set_sphere(IVec3::new(127, 64, 100), 4, Some(TEST_COL));
559 assert_eq!(g.chunk_count(), 2);
561
562 let v0 = g.chunk(IVec3::ZERO).unwrap();
563 assert!(voxel_is_solid(v0, 127, 64, 100));
566 assert!(voxel_is_solid(v0, 124, 64, 100));
568
569 let v1 = g.chunk(IVec3::new(1, 0, 0)).unwrap();
570 assert!(voxel_is_solid(v1, 0, 64, 100));
573 assert!(voxel_is_solid(v1, 2, 64, 100));
575 }
576
577 fn stamp_sentinel_cache(g: &mut Grid) {
585 g.billboards = Some(crate::BillboardCache::new_empty(32));
586 }
587
588 #[test]
589 fn set_voxel_invalidates_billboard_cache() {
590 let mut g = Grid::new(GridTransform::identity());
591 stamp_sentinel_cache(&mut g);
592 assert!(g.billboards.is_some());
593 g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
594 assert!(
595 g.billboards.is_none(),
596 "set_voxel should clear the billboard cache"
597 );
598 }
599
600 #[test]
601 fn set_voxel_carve_also_invalidates() {
602 let mut g = Grid::new(GridTransform::identity());
604 stamp_sentinel_cache(&mut g);
605 g.set_voxel(IVec3::new(5, 5, 5), None); assert!(
607 g.billboards.is_none(),
608 "carve should clear the cache (conservative)"
609 );
610 }
611
612 #[test]
613 fn set_rect_invalidates_billboard_cache() {
614 let mut g = Grid::new(GridTransform::identity());
615 stamp_sentinel_cache(&mut g);
616 g.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
617 assert!(g.billboards.is_none(), "set_rect should clear the cache");
618 }
619
620 #[test]
621 fn set_sphere_invalidates_billboard_cache() {
622 let mut g = Grid::new(GridTransform::identity());
623 stamp_sentinel_cache(&mut g);
624 g.set_sphere(IVec3::new(64, 64, 100), 5, Some(TEST_COL));
625 assert!(g.billboards.is_none(), "set_sphere should clear the cache");
626 }
627
628 #[test]
629 fn set_voxel_dispatches_to_correct_chunk_on_y_z_axes() {
630 let mut g = Grid::new(GridTransform::identity());
634 g.set_voxel(IVec3::new(200, 300, 500), Some(TEST_COL));
635 let vxl = g
636 .chunk(IVec3::new(1, 2, 1))
637 .expect("expected chunk (1, 2, 1)");
638 assert!(voxel_is_solid(vxl, 72, 44, 244));
639 }
640
641 #[test]
644 fn chunk_version_defaults_to_zero_for_missing() {
645 let g = Grid::new(GridTransform::identity());
646 assert_eq!(g.chunk_version(IVec3::ZERO), 0);
647 assert_eq!(g.chunk_version(IVec3::new(7, -3, 12)), 0);
648 }
649
650 #[test]
651 fn set_voxel_insert_bumps_to_one() {
652 let mut g = Grid::new(GridTransform::identity());
653 assert_eq!(g.chunk_version(IVec3::ZERO), 0);
654 g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
655 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
656 }
657
658 #[test]
659 fn set_voxel_carve_in_existing_chunk_bumps() {
660 let mut g = Grid::new(GridTransform::identity());
662 g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
663 g.set_voxel(IVec3::new(5, 5, 5), None);
664 assert_eq!(g.chunk_version(IVec3::ZERO), 2);
665 }
666
667 #[test]
668 fn set_voxel_carve_in_missing_chunk_does_not_bump() {
669 let mut g = Grid::new(GridTransform::identity());
671 g.set_voxel(IVec3::new(5, 5, 5), None);
672 assert_eq!(g.chunk_version(IVec3::ZERO), 0);
673 assert!(g.chunk_versions.is_empty());
674 }
675
676 #[test]
677 fn set_rect_multi_chunk_bumps_every_touched_chunk() {
678 let mut g = Grid::new(GridTransform::identity());
680 g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), Some(TEST_COL));
681 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
682 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
683 assert_eq!(g.chunk_versions.len(), 2);
685 }
686
687 #[test]
688 fn set_rect_carve_bumps_only_existing_chunks() {
689 let mut g = Grid::new(GridTransform::identity());
693 g.set_voxel(IVec3::new(0, 0, 0), Some(TEST_COL));
694 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
695 g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), None);
696 assert_eq!(g.chunk_version(IVec3::ZERO), 2);
697 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 0);
698 }
699
700 #[test]
707 fn set_sphere_with_colfunc_paints_exposed_interior() {
708 const CRATER: VoxColor = VoxColor(0x00_44_55_66);
709 let mut g = Grid::new(GridTransform::identity());
711 g.set_rect(
712 IVec3::new(40, 40, 40),
713 IVec3::new(90, 90, 90),
714 Some(TEST_COL),
715 );
716 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| {
719 CRATER
720 });
721
722 assert!(!g.voxel_solid(IVec3::new(64, 64, 64)));
724 assert!(g.voxel_solid(IVec3::new(64, 64, 55)));
727 assert_eq!(g.voxel_color(IVec3::new(64, 64, 55)), Some(CRATER));
728
729 let mut g2 = Grid::new(GridTransform::identity());
732 g2.set_rect(
733 IVec3::new(40, 40, 40),
734 IVec3::new(90, 90, 90),
735 Some(TEST_COL),
736 );
737 g2.set_sphere(IVec3::new(64, 64, 64), 8, None);
738 assert!(g2.voxel_solid(IVec3::new(64, 64, 55)));
739 assert_eq!(g2.voxel_color(IVec3::new(64, 64, 55)), None);
740 }
741
742 #[test]
748 fn set_sphere_with_colfunc_uses_grid_local_coords_across_chunks() {
749 #[allow(clippy::cast_sign_loss)]
751 let encode = |x: i32, y: i32, z: i32| VoxColor(((x << 16) | (y << 8) | z) as u32);
752
753 let mut g = Grid::new(GridTransform::identity());
754 g.set_rect(
756 IVec3::new(120, 60, 60),
757 IVec3::new(140, 80, 80),
758 Some(TEST_COL),
759 );
760 g.set_sphere_with_colfunc(IVec3::new(128, 70, 70), 5, SpanOp::Carve, |x, y, z| {
762 encode(x, y, z)
763 });
764
765 let p = IVec3::new(130, 70, 65);
770 assert!(g.voxel_solid(p));
771 #[allow(clippy::cast_sign_loss)]
772 let want = encode(130, 70, 65);
773 assert_eq!(g.voxel_color(p), Some(want));
774 #[allow(clippy::cast_sign_loss)]
776 let chunk_local = encode(2, 70, 65);
777 assert_ne!(g.voxel_color(p), Some(chunk_local));
778 }
779
780 #[test]
781 fn set_rect_with_colfunc_carve_paints_exposed_face() {
782 const WALL: VoxColor = VoxColor(0x00_12_34_56);
783 let mut g = Grid::new(GridTransform::identity());
784 g.set_rect(
785 IVec3::new(40, 40, 40),
786 IVec3::new(90, 90, 90),
787 Some(TEST_COL),
788 );
789 g.set_rect_with_colfunc(
791 IVec3::new(50, 50, 50),
792 IVec3::new(80, 80, 80),
793 SpanOp::Carve,
794 |_x, _y, _z| WALL,
795 );
796 assert!(!g.voxel_solid(IVec3::new(64, 64, 64)));
797 assert!(g.voxel_solid(IVec3::new(64, 64, 49)));
798 assert_eq!(g.voxel_color(IVec3::new(64, 64, 49)), Some(WALL));
799 }
800
801 #[test]
802 fn set_sphere_with_colfunc_invalidates_billboard_cache() {
803 let mut g = Grid::new(GridTransform::identity());
804 g.set_rect(
805 IVec3::new(40, 40, 40),
806 IVec3::new(90, 90, 90),
807 Some(TEST_COL),
808 );
809 stamp_sentinel_cache(&mut g);
810 g.set_sphere_with_colfunc(IVec3::new(64, 64, 64), 6, SpanOp::Carve, |_, _, _| {
811 VoxColor(1)
812 });
813 assert!(g.billboards.is_none());
814 }
815
816 #[test]
817 fn set_sphere_multi_chunk_bumps_every_written_chunk() {
818 let mut g = Grid::new(GridTransform::identity());
821 g.set_sphere(IVec3::new(127, 64, 100), 4, Some(TEST_COL));
822 assert_eq!(g.chunk_version(IVec3::ZERO), 1);
823 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
824 }
825}