From 0975cb46c79080e272e9fcb4c35c74d7c9777af2 Mon Sep 17 00:00:00 2001
From: Alejandro Perea <alexpro820@gmail.com>
Date: Mon, 25 Apr 2022 21:37:02 +0200
Subject: [PATCH] Add basic chunk utilities to infinite layers (#210)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Add basic chunk utils to infinite layers

* Mark TODOs

* Apply suggestions from code review

Co-authored-by: Thorbjørn Lindeijer <bjorn@lindeijer.nl>

* Apply interface suggestions from code review

* Update changelog

Co-authored-by: Thorbjørn Lindeijer <bjorn@lindeijer.nl>
---
 CHANGELOG.md                |   5 ++
 src/layers/tile/infinite.rs | 147 ++++++++++++++++++++++++++++++++----
 tests/lib.rs                |   2 +-
 3 files changed, 138 insertions(+), 16 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50ffa3a..fe131d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [0.10.2]
 ### Added
+- Map-wrapped chunks: `ChunkWrapper`.
+- Ability to access infinite tile layer chunks via `InfiniteTileLayer::chunks`, 
+`InfiniteTileLayerData::chunk_data`, `InfiniteTileLayerData::get_chunk_data` &
+`InfiniteTileLayer::get_chunk`, as well as chunk data via `Chunk::get_tile_data` &
+`ChunkWrapper::get_tile`.
 - `TileLayer::width` & `TileLayer::height` for ergonomic access of width/height.
 - `FiniteTileLayerData::get_tile_data`, `InfiniteTileLayerData::get_tile_data`.
 - `Default` derived implementation for `Loader` & `FilesystemResourceCache`
diff --git a/src/layers/tile/infinite.rs b/src/layers/tile/infinite.rs
index 01b5bb8..9c79e5b 100644
--- a/src/layers/tile/infinite.rs
+++ b/src/layers/tile/infinite.rs
@@ -41,7 +41,7 @@ impl InfiniteTileLayerData {
                 let chunk = InternalChunk::new(parser, attrs, e.clone(), c.clone(), tilesets)?;
                 for x in chunk.x..chunk.x + chunk.width as i32 {
                     for y in chunk.y..chunk.y + chunk.height as i32 {
-                        let chunk_pos = tile_to_chunk_pos(x, y);
+                        let chunk_pos = Chunk::tile_to_chunk_pos(x, y);
                         let relative_pos = (x - chunk_pos.0 * Chunk::WIDTH as i32, y - chunk_pos.1 * Chunk::HEIGHT as i32);
                         let chunk_index = (relative_pos.0 + relative_pos.1 * Chunk::WIDTH as i32) as usize;
                         let internal_pos = (x - chunk.x, y - chunk.y);
@@ -63,7 +63,7 @@ impl InfiniteTileLayerData {
     ///
     /// If you want to get a [`Tile`](`crate::Tile`) instead, use [`InfiniteTileLayer::get_tile()`].
     pub fn get_tile_data(&self, x: i32, y: i32) -> Option<&LayerTileData> {
-        let chunk_pos = tile_to_chunk_pos(x, y);
+        let chunk_pos = Chunk::tile_to_chunk_pos(x, y);
         self.chunks
             .get(&chunk_pos)
             .and_then(|chunk| {
@@ -76,30 +76,51 @@ impl InfiniteTileLayerData {
             })
             .flatten()
     }
-}
 
-fn tile_to_chunk_pos(x: i32, y: i32) -> (i32, i32) {
-    (
-        floor_div(x, Chunk::WIDTH as i32),
-        floor_div(y, Chunk::HEIGHT as i32),
-    )
+    /// Returns an iterator over only the data part of the chunks of this tile layer.
+    ///
+    /// In 99.99% of cases you'll want to use [`InfiniteTileLayer::chunks()`] instead; Using this method is only
+    /// needed if you *only* require the tile data of the chunks (and no other utilities provided by
+    /// the map-wrapped [`LayerTile`]), and you are in dire need for that extra bit of performance.
+    ///
+    /// This iterator doesn't have any particular order.
+    #[inline]
+    pub fn chunk_data(&self) -> impl ExactSizeIterator<Item = ((i32, i32), &Chunk)> {
+        self.chunks.iter().map(|(pos, chunk)| (*pos, chunk))
+    }
+
+    /// Obtains a chunk's data by its position. To obtain the position of the chunk that contains a
+    /// tile, use [Chunk::tile_to_chunk_pos()].
+    ///
+    /// In 99.99% of cases you'll want to use [`InfiniteTileLayer::get_chunk()`] instead; Using this method is only
+    /// needed if you *only* require the tile data of the chunk (and no other utilities provided by
+    /// the map-wrapped [`LayerTile`]), and you are in dire need for that extra bit of performance.
+    #[inline]
+    pub fn get_chunk_data(&self, x: i32, y: i32) -> Option<&Chunk> {
+        self.chunks.get(&(x, y))
+    }
 }
 
-/// Part of an infinite tile layer.
+/// Part of an infinite tile layer's data.
+///
+/// Has only the tile data contained within and not a reference to the map it is part of. It is for
+/// this reason that this type should actually be called `ChunkData`, and so this will change in
+/// the next breaking release. In 99.99% of cases you'll actually want to use [`ChunkWrapper`].
+// TODO(0.11.0): Rename to ChunkData
 #[derive(Debug, PartialEq, Clone)]
 pub struct Chunk {
     tiles: Box<[Option<LayerTileData>; Self::TILE_COUNT]>,
 }
 
 impl Chunk {
-    /// Internal infinite layer chunk width. Do not rely on this value as it might change between
-    /// versions.
+    /// Infinite layer chunk width. This constant might change between versions, not counting as a
+    /// breaking change.
     pub const WIDTH: u32 = 16;
-    /// Internal infinite layer chunk height. Do not rely on this value as it might change between
-    /// versions.
+    /// Infinite layer chunk height. This constant might change between versions, not counting as a
+    /// breaking change.
     pub const HEIGHT: u32 = 16;
-    /// Internal infinite layer chunk tile count. Do not rely on this value as it might change
-    /// between versions.
+    /// Infinite layer chunk tile count. This constant might change between versions, not counting
+    /// as a breaking change.
     pub const TILE_COUNT: usize = Self::WIDTH as usize * Self::HEIGHT as usize;
 
     pub(crate) fn new() -> Self {
@@ -107,6 +128,45 @@ impl Chunk {
             tiles: Box::new([None; Self::TILE_COUNT]),
         }
     }
+
+    /// Obtains the tile data present at the position given relative to the chunk's top-left-most tile.
+    ///
+    /// If the position given is invalid or the position is empty, this function will return [`None`].
+    ///
+    /// If you want to get a [`LayerTile`](`crate::LayerTile`) instead, use [`ChunkWrapper::get_tile()`].
+    pub fn get_tile_data(&self, x: i32, y: i32) -> Option<&LayerTileData> {
+        if x < Self::WIDTH as i32 && y < Self::HEIGHT as i32 && x >= 0 && y >= 0 {
+            self.tiles[x as usize + y as usize * Self::WIDTH as usize].as_ref()
+        } else {
+            None
+        }
+    }
+
+    /// Returns the position of the chunk that contains the given tile position.
+    pub fn tile_to_chunk_pos(x: i32, y: i32) -> (i32, i32) {
+        (
+            floor_div(x, Chunk::WIDTH as i32),
+            floor_div(y, Chunk::HEIGHT as i32),
+        )
+    }
+}
+
+// TODO(0.11.0): Rename to Chunk
+map_wrapper!(
+    #[doc = "Part of an [`InfiniteTileLayer`]."]
+    #[doc = "This is the only map-wrapped type that has a `Wrapper` suffix. This will change in a later version."]
+    ChunkWrapper => Chunk
+);
+
+impl<'map> ChunkWrapper<'map> {
+    /// Obtains the tile present at the position given relative to the chunk's top-left-most tile.
+    ///
+    /// If the position given is invalid or the position is empty, this function will return [`None`].
+    pub fn get_tile(&self, x: i32, y: i32) -> Option<LayerTile<'map>> {
+        self.data
+            .get_tile_data(x, y)
+            .map(|data| LayerTile::new(self.map(), data))
+    }
 }
 
 #[derive(Debug, PartialEq, Clone)]
@@ -167,4 +227,61 @@ impl<'map> InfiniteTileLayer<'map> {
             .get_tile_data(x, y)
             .map(|data| LayerTile::new(self.map, data))
     }
+
+    /// Returns an iterator over different parts of this map called [`Chunk`]s.
+    ///
+    /// These **may not** correspond with the chunks in the TMX file, as the chunk size is
+    /// implementation defined (see [`Chunk::WIDTH`], [`Chunk::HEIGHT`]).
+    ///
+    /// The iterator item contains the position of the chunk in chunk coordinates along with a
+    /// reference to the actual chunk at that position.
+    ///
+    /// This iterator doesn't have any particular order.
+    ///
+    /// ## Example
+    /// ```
+    /// # use tiled::{Loader, LayerType, TileLayer};
+    /// use tiled::Chunk;
+    ///
+    /// # let map = Loader::new()
+    /// #     .load_tmx_map("assets/tiled_base64_zlib_infinite.tmx")
+    /// #     .unwrap();
+    /// # if let LayerType::TileLayer(TileLayer::Infinite(infinite_layer)) =
+    /// #     &map.get_layer(0).unwrap().layer_type()
+    /// # {
+    /// for (chunk_pos, chunk) in infinite_layer.chunks() {
+    ///     for x in 0..Chunk::WIDTH as i32 {
+    ///         for y in 0..Chunk::HEIGHT as i32 {
+    ///             if let Some(tile) = chunk.get_tile(x, y) {
+    ///                 let tile_pos = (
+    ///                     chunk_pos.0 * Chunk::WIDTH as i32 + x,
+    ///                     chunk_pos.1 * Chunk::HEIGHT as i32 + y,
+    ///                 );
+    ///                 println!("At ({}, {}): {:?}", tile_pos.0, tile_pos.1, tile);
+    ///             }
+    ///         }
+    ///     }
+    /// }
+    /// # } else {
+    /// #     panic!("It is wrongly recognised as a finite map");
+    /// # }
+    /// ```
+    #[inline]
+    pub fn chunks(&self) -> impl ExactSizeIterator<Item = ((i32, i32), ChunkWrapper<'map>)> + 'map {
+        let map: &'map crate::Map = self.map;
+        self.data
+            .chunks
+            .iter()
+            .map(move |(pos, chunk)| (*pos, ChunkWrapper::new(map, chunk)))
+    }
+
+    /// Obtains a chunk by its position. To obtain the position of the chunk that contains a tile,
+    /// use [Chunk::tile_to_chunk_pos].
+    #[inline]
+    pub fn get_chunk(&self, x: i32, y: i32) -> Option<ChunkWrapper<'map>> {
+        let map: &'map crate::Map = self.map;
+        self.data
+            .get_chunk_data(x, y)
+            .map(move |data| ChunkWrapper::new(map, data))
+    }
 }
diff --git a/tests/lib.rs b/tests/lib.rs
index 1ab44a8..c0faf29 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -117,7 +117,7 @@ fn test_just_tileset() {
 }
 
 #[test]
-fn test_infinite_tileset() {
+fn test_infinite_map() {
     let r = Loader::new()
         .load_tmx_map("assets/tiled_base64_zlib_infinite.tmx")
         .unwrap();
-- 
GitLab