tiles_tools 0.2.0

High-performance tile-based game development toolkit with comprehensive coordinate systems (hexagonal, square, triangular, isometric), pathfinding, ECS integration, and grid management.
Documentation
//! This module provides a generic 2D grid structure, `Grid2D`, designed to store data
//! mapped to hexagonal coordinates. It is built upon `ndarray` and supports arbitrary
//! coordinate ranges and different hexagonal systems and orientations.

use crate::coordinates::hexagonal::Coordinate;
use ndarray_cg::{ Array2, I64x2, nd::iter::Iter };
use std::fmt::Debug;
use std::marker::PhantomData;
use std::ops::{ Index, IndexMut };

/// A generic 2D grid for storing data associated with hexagonal coordinates.
///
/// The grid is defined by a rectangular region of coordinates and can store any type `T`.
/// It is generic over the hexagonal coordinate system and orientation.
pub struct Grid2D< System, Orientation, T >
{
  data : Array2< T >,
  min : I64x2,
  _marker : PhantomData< Coordinate< System, Orientation > >,
}

impl< System, Orientation, T > Grid2D< System, Orientation, T >
{
  /// Creates a new grid with a specified size, filling it with values generated by a function.
  ///
  /// # Panics
  /// Panics if the size derived from the min/max coordinates is negative or cannot be converted to `usize`.
  pub fn with_size_and_fn< F >
  (
    min_inclusive : Coordinate< System, Orientation >,
    max_exclusive : Coordinate< System, Orientation >,
    f : F,
  )
  -> Self
  where
    F : Fn() -> T,
  {
    let Coordinate { q, r, .. } = min_inclusive;
    let min_q = q as i64;
    let min_r = r as i64;
    let Coordinate { q, r, .. } = max_exclusive;
    let max_q = q as i64;
    let max_r = r as i64;

    let columns : usize = ( max_q - min_q ).try_into().expect( "Invalid size" );
    let rows : usize = ( max_r - min_r ).try_into().expect( "Invalid size" );

    Self
    {
      data : Array2::from_shape_simple_fn( ( rows, columns ), f ),
      min : I64x2::from_array( [ min_q, min_r ] ),
      _marker : PhantomData,
    }
  }

  /// Returns an iterator over the values in the grid.
  pub fn iter( &self ) -> Iter< '_, T, ndarray_cg::Dim< [ usize; 2 ] > >
  {
    self.data.iter()
  }

  /// Returns an iterator that yields each coordinate and a reference to its corresponding value.
  pub fn indexed_iter( &self ) -> impl Iterator< Item = ( Coordinate< System, Orientation >, &T ) >
  {
    self.data.indexed_iter().map( | ( ( i, j ), value ) |
    {
      let i = ( i as i64 + self.min[ 1 ] ) as i32;
      let j = ( j as i64 + self.min[ 0 ] ) as i32;
      let coord = Coordinate::< System, Orientation >::new_uncheked( j, i );
      ( coord, value )
    })
  }
}

impl< System, Orientation, T > Grid2D< System, Orientation, T >
where
  T : Default,
{
  /// Creates a new grid with a specified size, filling it with the default value of `T`.
  ///
  /// # Panics
  /// Panics if the size derived from the min/max coordinates is negative or cannot be converted to `usize`.
  pub fn with_size_and_default
  (
    min_inclusive : Coordinate< System, Orientation >,
    max_exclusive : Coordinate< System, Orientation >,
  )
  -> Self
  {
    let Coordinate { q, r, .. } = min_inclusive;
    let min_q = q as i64;
    let min_r = r as i64;
    let Coordinate { q, r, .. } = max_exclusive;
    let max_q = q as i64;
    let max_r = r as i64;

    let columns : usize = ( max_q - min_q ).try_into().expect( "Invalid size" );
    let rows : usize = ( max_r - min_r ).try_into().expect( "Invalid size" );

    Self
    {
      data : Array2::from_shape_simple_fn( ( rows, columns ), T::default ),
      min : I64x2::from_array( [ min_q, min_r ] ),
      _marker : PhantomData,
    }
  }
}

impl< System, Orientation, T > Grid2D< System, Orientation, Option< T > >
{
  /// Inserts a value at the given coordinate, returning the previous value if any.
  ///
  /// # Panics
  /// Panics if the coordinate is out of the grid's bounds.
  pub fn insert< C >( &mut self, coord : C, value : T ) -> Option< T >
  where
    C : Into< Coordinate< System, Orientation > >,
  {
    let coord : Coordinate< System, Orientation > = coord.into();
    let i : usize = ( coord.r as i64 - self.min[ 1 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    let j : usize = ( coord.q as i64 - self.min[ 0 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    std::mem::replace( &mut self.data[ ( i, j ) ], Some( value ) )
  }

  /// Removes and returns the value at the given coordinate, leaving `None` in its place.
  ///
  /// # Panics
  /// Panics if the coordinate is out of the grid's bounds.
  pub fn remove< C >( &mut self, coord : C ) -> Option< T >
  where
    C : Into< Coordinate< System, Orientation > >,
  {
    let coord : Coordinate< System, Orientation > = coord.into();
    let i : usize = ( coord.r as i64 - self.min[ 1 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    let j : usize = ( coord.q as i64 - self.min[ 0 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    std::mem::take( &mut self.data[ ( i, j ) ] )
  }

  /// Returns an immutable reference to the value at a coordinate, if it exists.
  ///
  /// # Panics
  /// Panics if the coordinate is out of the grid's bounds.
  pub fn get< C >( &self, coord : C ) -> Option< &T >
  where
    C : Into< Coordinate< System, Orientation > >,
  {
    let coord : Coordinate< System, Orientation > = coord.into();
    let i : usize = ( coord.r as i64 - self.min[ 1 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    let j : usize = ( coord.q as i64 - self.min[ 0 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    self.data.get( ( i, j ) ).and_then( | o | o.as_ref() )
  }

  /// Returns a mutable reference to the value at a coordinate, if it exists.
  ///
  /// # Panics
  /// Panics if the coordinate is out of the grid's bounds.
  pub fn get_mut< C >( &mut self, coord : C ) -> Option< &mut T >
  where
    C : Into< Coordinate< System, Orientation > >,
  {
    let coord : Coordinate< System, Orientation > = coord.into();
    let i : usize = ( coord.r as i64 - self.min[ 1 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    let j : usize = ( coord.q as i64 - self.min[ 0 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    self.data.get_mut( ( i, j ) ).and_then( | o | o.as_mut() )
  }
}

impl< C, System, Orientation, T > Index< C > for Grid2D< System, Orientation, T >
where
  C : Into< Coordinate< System, Orientation > >,
{
  type Output = T;

  /// Accesses the value at the given coordinate.
  ///
  /// # Panics
  /// Panics if the coordinate is out of the grid's bounds.
  fn index( &self, index : C ) -> &Self::Output
  {
    let coord : Coordinate< System, Orientation > = index.into();
    let i : usize = ( coord.r as i64 - self.min[ 1 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    let j : usize = ( coord.q as i64 - self.min[ 0 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    self.data.index( ( i, j ) )
  }
}

impl< C, System, Orientation, T > IndexMut< C > for Grid2D< System, Orientation, T >
where
  C : Into< Coordinate< System, Orientation > >,
{
  /// Mutably accesses the value at the given coordinate.
  ///
  /// # Panics
  /// Panics if the coordinate is out of the grid's bounds.
  fn index_mut( &mut self, index : C ) -> &mut Self::Output
  {
    let coord : Coordinate< System, Orientation > = index.into();
    let i : usize = ( coord.r as i64 - self.min[ 1 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    let j : usize = ( coord.q as i64 - self.min[ 0 ] )
    .try_into()
    .expect( "Coordinate out of bound" );
    self.data.index_mut( ( i, j ) )
  }
}

impl< System, Orientation, T > Clone for Grid2D< System, Orientation, T >
where
  T : Clone,
{
  /// Clones the grid and its data.
  fn clone( &self ) -> Self
  {
    Self
    {
      data : self.data.clone(),
      min : self.min,
      _marker : PhantomData,
    }
  }
}

impl< System, Orientation, T > Debug for Grid2D< System, Orientation, T >
where
  T : Debug,
{
  /// Formats the grid for debugging purposes.
  fn fmt( &self, f : &mut std::fmt::Formatter< '_ > ) -> std::fmt::Result
  {
    f.debug_struct( "Grid2D" )
    .field( "data", &self.data )
    .field( "min", &self.min )
    .field( "_marker", &self._marker )
    .finish()
  }
}