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