1use all_is_cubes_base::math::{GridSize, Octant};
4use alloc::collections::BTreeMap;
5use alloc::sync::Arc;
6use alloc::vec::Vec;
7use core::fmt;
8use core::iter::FusedIterator;
9use core::ops::RangeTo;
10
11#[cfg(feature = "std")]
12use std::sync::Mutex;
13
14use euclid::{Point3D, Vector3D};
15
16#[cfg(not(any(feature = "std", test)))]
18#[allow(
19 unused_imports,
20 reason = "unclear why this warns even though it is needed"
21)]
22use num_traits::float::FloatCore as _;
23
24#[cfg(not(any(feature = "std", test)))]
25#[allow(
26 unused_imports,
27 reason = "unclear why this warns even though it is needed"
28)]
29use crate::math::Euclid as _;
30use crate::math::{
31 Cube, FreeCoordinate, FreePoint, GridAab, GridCoordinate, GridPoint, GridSizeCoord, OctantMask,
32};
33
34#[derive(Debug)]
36enum WholeChunk {}
37
38#[expect(clippy::exhaustive_enums)]
40#[derive(Debug)]
41pub enum ChunkRelative {}
42
43type Ccv = Vector3D<i32, WholeChunk>;
45
46#[derive(Clone, Copy, Eq, Hash, PartialEq)]
56#[expect(clippy::exhaustive_structs)]
57pub struct ChunkPos<const CHUNK_SIZE: GridCoordinate>(pub Cube);
58
59impl<const CHUNK_SIZE: GridCoordinate> fmt::Debug for ChunkPos<CHUNK_SIZE> {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 let Self(Cube { x, y, z }) = *self;
62 write!(f, "ChunkPos<{CHUNK_SIZE}>({x}, {y}, {z})")
63 }
64}
65
66impl<const CHUNK_SIZE: GridCoordinate> ChunkPos<CHUNK_SIZE> {
67 pub const fn new(x: GridCoordinate, y: GridCoordinate, z: GridCoordinate) -> Self {
70 Self(Cube::new(x, y, z))
71 }
72
73 pub fn bounds(self) -> GridAab {
77 GridAab::from_lower_size(
78 self.0.lower_bounds() * CHUNK_SIZE,
79 GridSize::splat(CHUNK_SIZE.cast_unsigned()),
80 )
81 }
82
83 pub fn distance(self, other: Self) -> Distance {
86 chunk_distance_squared_for_view((self.0 - other.0).cast_unit())
87 }
88
89 pub fn min_distance_squared_from(self, origin_chunk: Self) -> GridCoordinate {
95 self.distance(origin_chunk).nearest_approach_squared.cast_signed() * CHUNK_SIZE.pow(2)
98 }
99}
100
101pub fn cube_to_chunk<const CHUNK_SIZE: GridCoordinate>(
105 cube: Cube,
106) -> (ChunkPos<CHUNK_SIZE>, Point3D<GridCoordinate, ChunkRelative>) {
107 let pos = cube.lower_bounds();
108 (
109 ChunkPos(Cube::from(pos.map(|c| c.div_euclid(CHUNK_SIZE)))),
110 pos.map(|c| c.rem_euclid(CHUNK_SIZE)).cast_unit(),
111 )
112}
113
114pub fn point_to_chunk<const CHUNK_SIZE: GridCoordinate>(point: FreePoint) -> ChunkPos<CHUNK_SIZE> {
116 ChunkPos(
117 Cube::containing(
118 point.map(|c| c.div_euclid(FreeCoordinate::from(CHUNK_SIZE))),
119 ).unwrap(),
120 )
121}
122
123#[derive(Copy, Clone, Eq, Ord, PartialEq, PartialOrd)]
133pub struct Distance {
134 nearest_approach_squared: u32,
140 off_plane_count: u8,
148}
149
150impl fmt::Debug for Distance {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 let Distance {
153 nearest_approach_squared,
154 off_plane_count,
155 } = self;
156 write!(f, "{nearest_approach_squared}+{off_plane_count}")
157 }
158}
159
160#[derive(Clone, Debug, Eq, PartialEq)] pub struct ChunkChart<const CHUNK_SIZE: GridCoordinate> {
166 view_distance_in_squared_chunks: GridSizeCoord,
169
170 octant_chunks: Arc<[Ccv]>,
189
190 octant_range: RangeTo<usize>,
192}
193
194impl<const CHUNK_SIZE: GridCoordinate> ChunkChart<CHUNK_SIZE> {
195 pub fn new(view_distance: FreeCoordinate) -> Self {
199 let view_distance_in_squared_chunks = Self::sanitize_and_square_distance(view_distance);
200 let octant_chunks = get_or_compute_chart_octant(view_distance_in_squared_chunks);
201 Self {
202 view_distance_in_squared_chunks,
203 octant_range: ..octant_chunks.len(),
204 octant_chunks,
205 }
206 }
207
208 fn sanitize_and_square_distance(view_distance: FreeCoordinate) -> GridSizeCoord {
209 let sanitized = if view_distance.is_finite() {
210 view_distance.max(0.)
211 } else {
212 0.
213 };
214 let view_distance_in_chunks = sanitized / FreeCoordinate::from(CHUNK_SIZE);
215
216 view_distance_in_chunks.powi(2).ceil() as GridSizeCoord
217 }
218
219 pub fn resize_if_needed(&mut self, view_distance: FreeCoordinate) {
221 if Self::sanitize_and_square_distance(view_distance) != self.view_distance_in_squared_chunks
222 {
223 *self = Self::new(view_distance);
226 }
227 }
228
229 pub fn chunks(
240 &self,
241 origin: ChunkPos<CHUNK_SIZE>,
242 mask: OctantMask,
243 ) -> impl DoubleEndedIterator<Item = ChunkPos<CHUNK_SIZE>> + FusedIterator + '_ {
244 self.octant_chunks[self.octant_range]
245 .iter()
246 .copied()
247 .flat_map(move |v| AxisMirrorIter::new(v, mask))
248 .map(move |v| ChunkPos(origin.0 + v.cast_unit()))
249 }
250
251 #[doc(hidden)]
253 #[mutants::skip]
254 pub fn visualization(&self) -> crate::space::Space {
255 use crate::block::Block;
256 use crate::math::Rgba;
257
258 let mut max = GridPoint::origin();
259 for chunk in self.octant_chunks[self.octant_range].iter().copied() {
260 max = max.max(chunk.to_point().cast_unit());
261 }
262 let extent = GridAab::from_lower_upper(max.map(|c| -c - 1), max.map(|c| c + 2));
263
264 let base_octant_chunk = Block::builder()
266 .display_name("Base octant chunk")
267 .color(Rgba::new(0.75, 0.4, 0.4, 1.0))
268 .build();
269 let other_octant_chunk_1 = Block::builder()
270 .display_name("Mirrored octant chunk")
271 .color(Rgba::new(0.5, 0.5, 0.5, 1.0))
272 .build();
273 let other_octant_chunk_2 = Block::builder()
274 .display_name("Mirrored octant chunk")
275 .color(Rgba::new(0.6, 0.6, 0.6, 1.0))
276 .build();
277
278 crate::space::Space::builder(extent)
279 .build_and_mutate(|m| {
280 for ChunkPos(pos) in self.chunks(ChunkPos::new(0, 0, 0), OctantMask::ALL) {
281 let px = pos.x >= 0;
282 let py = pos.y >= 0;
283 let pz = pos.z >= 0;
284 let block = if px && py && pz {
285 &base_octant_chunk
286 } else if px ^ py ^ pz {
287 &other_octant_chunk_1
288 } else {
289 &other_octant_chunk_2
290 };
291 m.set(pos, block).unwrap();
292 }
293 Ok(())
294 })
295 .unwrap()
296 }
297
298 #[doc(hidden)] pub fn count_all(&self) -> usize {
302 self.octant_range.end * 8
304 }
305}
306
307fn get_or_compute_chart_octant(view_distance_in_squared_chunks: GridSizeCoord) -> Arc<[Ccv]> {
308 #[cfg(feature = "std")]
309 let mut cache = match CHUNK_CHART_CACHE.lock() {
310 Ok(cache) => cache,
311 Err(p) => {
312 let mut cache = p.into_inner();
314 *cache = BTreeMap::new();
315 cache
316 }
317 };
318
319 #[cfg(not(feature = "std"))]
321 let mut cache = BTreeMap::new();
322
323 let len = cache.len();
324
325 use alloc::collections::btree_map::Entry;
326 match cache.entry(view_distance_in_squared_chunks) {
327 Entry::Occupied(e) => {
328 Arc::clone(e.get())
330 }
331 Entry::Vacant(e) => {
332 let value = compute_chart_octant(view_distance_in_squared_chunks);
334
335 if len < 20 {
338 e.insert(Arc::clone(&value));
339 }
340
341 value
342 }
343 }
344}
345
346#[doc(hidden)] pub fn reset_chunk_chart_cache() {
348 #[cfg(feature = "std")]
349 match CHUNK_CHART_CACHE.lock() {
350 Ok(mut cache) => cache.clear(),
351 Err(_) => {
352 }
354 }
355}
356
357fn compute_chart_octant(view_distance_in_squared_chunks: GridSizeCoord) -> Arc<[Ccv]> {
358 let candidates = GridAab::from_lower_size(
363 [0, 0, 0],
364 GridSize::splat(view_distance_in_squared_chunks.saturating_add(1)),
365 )
366 .to_vol()
367 .unwrap();
368 let mut octant_chunks: Vec<Ccv> = Vec::with_capacity(candidates.volume());
369 for chunk_cube in candidates.iter_cubes() {
371 let chunk = chunk_cube.lower_bounds().to_vector().cast_unit();
372 if chunk_distance_squared_for_view(chunk).nearest_approach_squared
373 <= view_distance_in_squared_chunks
374 {
375 octant_chunks.push(chunk);
376 }
377 }
378
379 octant_chunks.sort_unstable_by_key(depth_sort_key);
380 octant_chunks.into()
381}
382
383fn depth_sort_key(&chunk: &Ccv) -> (Distance, [i32; 3]) {
386 (chunk_distance_squared_for_view(chunk), chunk.into())
387}
388
389fn chunk_distance_squared_for_view(chunk: Ccv) -> Distance {
390 let chunk = chunk.map(i32::unsigned_abs);
391 Distance {
398 nearest_approach_squared: chunk
399 .map(
400 #[inline(always)]
401 |s| s.saturating_sub(1),
402 )
403 .square_length(),
404 off_plane_count: u8::from(chunk.x > 0) + u8::from(chunk.y > 0) + u8::from(chunk.z > 0),
405 }
406}
407
408#[cfg(feature = "std")]
412static CHUNK_CHART_CACHE: Mutex<BTreeMap<GridSizeCoord, Arc<[Ccv]>>> = Mutex::new(BTreeMap::new());
413
414struct AxisMirrorIter {
418 v: Ccv,
419 todo: OctantMask,
421}
422impl AxisMirrorIter {
423 #[inline]
424 fn new(v: Ccv, mask: OctantMask) -> Self {
425 let todo = mask.collapse_to_negative(v.x == 0, v.y == 0, v.z == 0);
428 Self { v, todo }
429 }
430
431 fn generate_and_clear(&mut self, octant: Octant) -> Ccv {
432 self.todo.clear(octant);
433 octant.reflect(self.v)
434 }
435}
436impl Iterator for AxisMirrorIter {
437 type Item = Ccv;
438 #[inline]
439 fn next(&mut self) -> Option<Ccv> {
440 self.todo.first().map(|index| self.generate_and_clear(index))
441 }
442
443 }
451impl DoubleEndedIterator for AxisMirrorIter {
452 #[inline]
453 fn next_back(&mut self) -> Option<Self::Item> {
454 self.todo.last().map(|index| self.generate_and_clear(index))
455 }
456}
457impl ExactSizeIterator for AxisMirrorIter {}
458impl FusedIterator for AxisMirrorIter {}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use crate::raytracer::print_space;
464 use pretty_assertions::assert_eq;
465 use rand::{Rng, SeedableRng};
466 use std::collections::HashSet;
467
468 #[test]
469 fn chunk_consistency() {
470 for cube in GridAab::from_lower_size([-1, -1, -1], [32, 32, 32]).interior_iter() {
472 let (chunk, rel) = cube_to_chunk::<16>(cube);
473 assert!(chunk.bounds().contains_cube(cube));
474 assert_eq!(
475 cube,
476 Cube::from(rel.cast_unit::<Cube>()) + chunk.bounds().lower_bounds().to_vector()
477 );
478 }
479 }
480
481 #[test]
482 fn min_distance_squared_cases() {
483 fn test(pos: [GridCoordinate; 3]) -> GridCoordinate {
484 let origin_cube = Cube::new(100, 200, 300);
486 const CS: GridCoordinate = 32;
488 let grid_distance = ChunkPos::<CS>(origin_cube + Vector3D::from(pos))
489 .min_distance_squared_from(ChunkPos(origin_cube));
490 assert_eq!(grid_distance.rem_euclid(CS.pow(2)), 0);
491 grid_distance / CS.pow(2)
492 }
493 assert_eq!(0, test([0, 0, 0]));
495 assert_eq!(0, test([1, 0, 0]));
496 assert_eq!(0, test([-1, 0, 0]));
497 assert_eq!(0, test([1, 1, 1]));
498 assert_eq!(1, test([2, 0, 0]));
500 assert_eq!(3, test([2, 2, 2]));
502 assert_eq!(3, test([-2, 2, 2]));
504 assert_eq!(3, test([-2, -2, 2]));
505 }
506
507 #[test]
508 #[ignore = "unimplemented"]
509 fn min_distance_squared_consistent_with_chart() {
510 todo!("implement check that min_distance_squared_from matches ChunkChart");
511 }
512
513 #[test]
518 fn chunk_chart_zero_size() {
519 let chart = ChunkChart::<16>::new(0.0);
520 let chunk = ChunkPos::new(1, 2, 3);
521 assert_eq!(
522 chart.chunks(chunk, OctantMask::ALL).collect::<Vec<_>>(),
523 vec![chunk]
524 );
525 assert!(dbg!(chart.count_all()) >= 1);
526 }
527
528 #[test]
530 fn chunk_chart_epsilon_size() {
531 let chart = ChunkChart::<16>::new(0.00001);
532 assert_eq!(
533 chart.chunks(ChunkPos::new(0, 0, 0), OctantMask::ALL).collect::<Vec<_>>(),
534 vec![
535 ChunkPos::new(0, 0, 0),
536 ChunkPos::new(0, 0, -1),
538 ChunkPos::new(0, 0, 1),
539 ChunkPos::new(0, -1, 0),
540 ChunkPos::new(0, 1, 0),
541 ChunkPos::new(-1, 0, 0),
542 ChunkPos::new(1, 0, 0),
543 ChunkPos::new(0, -1, -1),
545 ChunkPos::new(0, -1, 1),
546 ChunkPos::new(0, 1, -1),
547 ChunkPos::new(0, 1, 1),
548 ChunkPos::new(-1, 0, -1),
549 ChunkPos::new(-1, 0, 1),
550 ChunkPos::new(1, 0, -1),
551 ChunkPos::new(1, 0, 1),
552 ChunkPos::new(-1, -1, 0),
553 ChunkPos::new(-1, 1, 0),
554 ChunkPos::new(1, -1, 0),
555 ChunkPos::new(1, 1, 0),
556 ChunkPos::new(-1, -1, -1),
558 ChunkPos::new(-1, -1, 1),
559 ChunkPos::new(-1, 1, -1),
560 ChunkPos::new(-1, 1, 1),
561 ChunkPos::new(1, -1, -1),
562 ChunkPos::new(1, -1, 1),
563 ChunkPos::new(1, 1, -1),
564 ChunkPos::new(1, 1, 1),
565 ]
566 );
567 assert!(dbg!(chart.count_all()) >= 29);
568 }
569
570 #[test]
572 fn chunk_chart_masked() {
573 let chart = ChunkChart::<16>::new(0.00001);
574 assert_eq!(
575 chart
576 .chunks(
577 ChunkPos::new(0, 0, 0),
578 OctantMask::from_iter([Octant::Ppp, Octant::Ppn, Octant::Pnn])
580 )
581 .collect::<Vec<_>>(),
582 vec![
583 ChunkPos::new(0, 0, 0),
584 ChunkPos::new(0, 0, -1),
586 ChunkPos::new(0, 0, 1),
587 ChunkPos::new(0, -1, 0),
588 ChunkPos::new(0, 1, 0),
589 ChunkPos::new(1, 0, 0),
590 ChunkPos::new(0, -1, -1),
592 ChunkPos::new(0, 1, -1),
594 ChunkPos::new(0, 1, 1),
595 ChunkPos::new(1, 0, -1),
598 ChunkPos::new(1, 0, 1),
599 ChunkPos::new(1, -1, 0),
602 ChunkPos::new(1, 1, 0),
603 ChunkPos::new(1, -1, -1),
605 ChunkPos::new(1, 1, -1),
606 ChunkPos::new(1, 1, 1),
607 ]
608 )
609 }
610 #[test]
611 fn chunk_chart_radius_break_points() {
612 fn assert_count(distance_in_chunks: FreeCoordinate, count: usize) {
613 let chart = ChunkChart::<16>::new(distance_in_chunks * 16.);
614
615 println!("distance {distance_in_chunks}, expected count {count}");
616 print_space(&chart.visualization().read(), [1., 1., 1.]);
617
618 let chunks: Vec<_> = chart.chunks(ChunkPos::new(0, 0, 0), OctantMask::ALL).collect();
619 assert_eq!(
620 chunks.len(),
621 count,
622 "distance = {}, octant_chunks = {:#?}, results = {:#?}",
623 distance_in_chunks,
624 chart.octant_chunks,
625 chunks
626 );
627 }
628 assert_count(0.00, 1);
629 assert_count(0.01, 3 * 3 * 3);
631 assert_count(0.99, 3 * 3 * 3);
632 assert_count(1.00, 3 * 3 * 3);
633 assert_count(1.01, 3 * 3 * 3 + 3 * 3 * 6 + 3 * 12);
638 }
639
640 #[test]
642 fn chunk_chart_reverse_iteration() {
643 let chart = ChunkChart::<16>::new(7. * 16.);
644 let p = ChunkPos::new(10, 3, 100);
645 let forward = chart.chunks(p, OctantMask::ALL).collect::<Vec<_>>();
646 let mut reverse = chart.chunks(p, OctantMask::ALL).rev().collect::<Vec<_>>();
647 reverse.reverse();
648 assert_eq!(forward, reverse);
649 }
650
651 #[test]
652 fn chunk_chart_sorting() {
653 let chart = ChunkChart::<16>::new(4.0 * 16.);
655 println!("{chart:?}");
656
657 let mut seen: HashSet<Cube> = HashSet::new();
658 for ChunkPos(p) in chart.chunks(ChunkPos::new(0, 0, 0), OctantMask::ALL) {
659 for &q in &seen {
660 assert!(
661 p.lower_bounds().map(|s| s.abs()) == q.lower_bounds().map(|s| s.abs())
663 || p.x.abs() > q.x.abs()
665 || p.y.abs() > q.y.abs()
666 || p.z.abs() > q.z.abs(),
667 "{p:?} should be before {q:?}"
668 );
669 }
670 seen.insert(p);
671 }
672 }
673
674 #[test]
677 fn chunk_chart_resize() {
678 let chart1 = ChunkChart::<16>::new(200.0);
679 let mut chart2 = ChunkChart::new(300.0);
680 chart2.resize_if_needed(200.0);
681 assert_eq!(
682 chart1.chunks(ChunkPos::new(0, 0, 0), OctantMask::ALL).collect::<Vec<_>>(),
683 chart2.chunks(ChunkPos::new(0, 0, 0), OctantMask::ALL).collect::<Vec<_>>()
684 );
685 }
686
687 #[test]
689 #[ignore = "TODO: enable this when we have cleverer resizing that might be wrong"]
690 fn chunk_chart_resize_rand() {
691 let mut rng = rand_xoshiro::Xoshiro256Plus::seed_from_u64(0);
692 for _ in 0..50 {
693 let target_size = rng.random_range(0.0..200.0);
694 let small_size = rng.random_range(0.0..target_size);
695 let big_size = rng.random_range(target_size..200.0);
696 print!("{small_size:?} -> {target_size:?} <- {big_size:?} | ");
697
698 let exact = ChunkChart::<16>::new(target_size);
699 let mut enlarged = ChunkChart::<16>::new(small_size);
700 enlarged.resize_if_needed(target_size);
701 let mut shrunk = ChunkChart::new(big_size);
702 shrunk.resize_if_needed(target_size);
703
704 println!(
705 "enlarged {} exact {} shrunk {}",
706 enlarged.octant_range.end, exact.octant_range.end, shrunk.octant_range.end
707 );
708
709 assert_eq!(
711 &enlarged.octant_chunks[enlarged.octant_range],
712 &exact.octant_chunks[exact.octant_range]
713 );
714 assert_eq!(
715 &shrunk.octant_chunks[shrunk.octant_range],
716 &exact.octant_chunks[exact.octant_range]
717 );
718
719 assert_eq!(
721 enlarged.chunks(ChunkPos::new(0, 0, 0), OctantMask::ALL).collect::<Vec<_>>(),
722 shrunk.chunks(ChunkPos::new(0, 0, 0), OctantMask::ALL).collect::<Vec<_>>()
723 );
724 }
725 }
726}