Source code for engine.util.grid

"""Abstract grid representations."""

from pygame import Rect


[docs]class Grid (object): """A representation of a 2D grid of rectangular integer-sized tiles. Used for aligning mouse input, graphics, etc. on a grid. Grid(ntiles, tile_size, gap = 0) :arg ntiles: ``(x, y)`` number of tiles in the grid, or a single number for a square grid. :arg tile_size: ``(tile_width, tile_height)`` integers giving the size of every tile, or a single number for square tiles. ``tile_width`` and ``tile_height`` can also be functions that take the column/row index and return the width/height of that column/row respectively, or lists (or anything supporting indexing) that perform the same task. :arg gap: ``(col_gap, row_gap)`` integers giving the gap between columns and rows respectively, or a single number for the same gap in both cases. As with ``tile_size``, this can be a tuple of functions (or lists) which take the index of the preceding column/row and return the gap size. ``col`` and ``row`` arguments to all methods may be negative to wrap from the end of the row/column, like list indices. """ def __init__ (self, ntiles, tile_size, gap = 0): if isinstance(ntiles, int): ntiles = (ntiles, ntiles) else: ntiles = tuple(ntiles[:2]) #: The ``(x, y)`` number of tiles in the grid. self.ntiles = ntiles def expand (obj, length): # expand an int/list/function to the given length if isinstance(obj, int): return (obj,) * length elif callable(obj): return tuple(obj(i) for i in xrange(length)) else: return tuple(obj[:length]) if isinstance(tile_size, int) or callable(tile_size): tx = ty = tile_size else: tx, ty = tile_size self._tile_size = (expand(tx, ntiles[0]), expand(ty, ntiles[1])) if isinstance(gap, int) or callable(tile_size): gx = gy = gap else: gx, gy = gap self._gap = (expand(gx, ntiles[0] - 1), expand(gy, ntiles[1] - 1)) @property def ncols (self): """The number of tiles in a row.""" return self.ntiles[0] @property def nrows (self): """The number of tiles in a column.""" return self.ntiles[1] def _size (self, axis): return sum(self._tile_size[axis]) + sum(self._gap[axis]) @property def w (self): """The total width of the grid.""" return self._size(0) @property def h (self): """The total height of the grid.""" return self._size(1) @property def size (self): """The total ``(width, height)`` size of the grid.""" return (self.w, self.h) def _tile_pos (self, axis, index): return sum(ts + gap for ts, gap in zip(self._tile_size[axis][:index], self._gap[axis][:index]))
[docs] def tile_x (self, col): """Get the x position of the tile in the column with the given index. This is the position of the left side of the tile relative to the left side of the grid. """ return self._tile_pos(0, col)
[docs] def tile_y (self, row): """Get the y position of the tile in the row with the given index. This is the position of the top side of the tile relative to the top side of the grid. """ return self._tile_pos(1, row)
[docs] def tile_pos (self, col, row): """Get the ``(x, y)`` position of the tile in the given column and row. This is the top-left corner of the tile relative to the top-left corner of the grid. """ return (self.tile_x(col), self.tile_y(row))
[docs] def tile_size (self, col, row): """Get the ``(width, height)`` size of the given tile.""" return (self._tile_size[0][col], self._tile_size[1][row])
[docs] def tile_rect (self, col, row): """Get a Pygame rect for the tile in the given column and row. This is relative to the top-left corner of the grid. """ return Rect(self.tile_pos(col, row), self.tile_size(col, row))
[docs] def tile_rects (self, pos=False): """Iterator over :meth:`tile_rect` for all tiles. :arg pos: whether to yield ``(col, row, tile_rect)`` instead of just ``tile_rect``. """ ts = self._tile_size gap = self._gap x = 0 # add extra element to gap so we iterate over the last tile for col, (w, gap_x) in enumerate(zip(ts[0], gap[0] + (0,))): y = 0 for row, (h, gap_y) in enumerate(zip(ts[1], gap[1] + (0,))): r = Rect(x, y, w, h) yield (col, row, r) if pos else r y += h + gap_y x += w + gap_x
[docs] def tile_at (self, x, y): """Return the ``(col, row)`` tile at the point ``(x, y)``, or ``None``.""" if x < 0 or y < 0: return None pos = (x, y) tile = [] for axis, pos in enumerate((x, y)): current_pos = 0 ts = self._tile_size[axis] gap = self._gap[axis] + (0,) for i in xrange(self.ntiles[axis]): current_pos += ts[i] # now we're at the end of a tile if current_pos > pos: # pos is within the previous tile tile.append(i) break current_pos += gap[i] # now we're at the start of a tile if current_pos > pos: # pos is within the previous gap return None else: # didn't find a tile: point is past the end return None return tuple(tile)
[docs] def align (self, graphic, col, row, alignment=0, pad=0, offset=0): """Align a graphic or surface within a tile. align(self, graphic, col, row, alignment=0, pad=0, offset=0) -> aligned_rect ``alignment``, ``pad`` and ``offset`` are as taken by :func:`align_rect <engine.util.align_rect>`. :arg graphic: a :class:`gfx.Graphic <engine.gfx.graphic.Graphic>` instance or a Pygame surface. In the former case, the graphic is moved (but it is not cropped to fit in the tile). :arg col: column of the tile. :arg row: row of the tile. :return: a Pygame rect clipped within the tile giving the area the graphic should be put in. """ if isinstance(graphic, Graphic): rect = graphic.rect else: rect = graphic.get_rect() pos = align_rect(rect, self.tile_rect(col, row), alignment, pad, offset) if isinstance(graphic, Graphic): graphic.pos = pos return Rect(pos, rect.size)
[docs]class InfiniteGrid (object): """A representation of an infinite 2D grid of rectangular tiles. InfiniteGrid(tile_size, gap=0) :arg tile_size: ``(tile_width, tile_height)`` numbers giving the size of every tile, or a single number for square tiles. :arg gap: ``(col_gap, row_gap)`` numbers giving the gap between columns and rows respectively, or a single number for the same gap in both cases. The grid expands in all directions, so ``col`` and ``row`` arguments to methods may be negative, and tile/gap sizes may be floats. """ def __init__ (self, tile_size, gap=0): if isinstance(tile_size, (int, float)): tile_size = (tile_size, tile_size) else: tile_size = tuple(tile_size[:2]) if any(x < 0 for x in tile_size): raise ValueError('tile sizes must be positive') #: ``tile_size`` as taken by the constructor. self.tile_size = tile_size if isinstance(gap, (int, float)): gap = (gap, gap) else: gap = tuple(gap[:2]) if any(g < 0 for g in gap): raise ValueError('tile gaps must be positive') #: ``gap`` as taken by the constructor. self.gap = gap
[docs] def tile_x (self, col): """Get the x position of the tile in the column with the given index. This is the position of the left side of the tile relative to the left side of column ``0``. """ return (self.tile_size[0] * self.gap[0]) * col
[docs] def tile_y (self, row): """Get the y position of the tile in the row with the given index. This is the position of the top side of the tile relative to the top side of row ``0``. """ return (self.tile_size[1] * self.gap[1]) * row
[docs] def tile_pos (self, col, row): """Get the ``(x, y)`` position of the tile in the given column and row. This is the top-left corner of the tile relative to the top-left corner of the tile ``(0, 0)``. """ return (self.tile_x(col), self.tile_y(row))
[docs] def tile_rect (self, col, row): """Get a Pygame-style rect for the tile in the given column and row. This is relative to tile ``(0, 0)``, and elements can be floats. """ return self.tile_pos(col, row) + self.tile_size
[docs] def tile_rects (self, rect, pos=False): """Iterator over :meth:`tile_rect` for tiles that intersect ``rect``. :arg rect: ``(x, y, w, h)`` with elements possibly floats. :arg pos: whether to yield ``(col, row, tile_rect)`` instead of just ``tile_rect``. """ ts = self.tile_size gap = self.gap # compute offsets x0 = (rect[0] // (ts[0] + gap[0])) * (ts[0] + gap[0]) y0 = (rect[1] // (ts[1] + gap[1])) * (ts[1] + gap[1]) # do the loop xr = rect[0] + rect[2] yb = rect[1] + rect[3] x = x0 col = 0 while True: y = y0 row = 0 while True: yield (col, row, r) if pos else r y += ts[1] + gap[1] if y >= yb: break row += 1 x += ts[0] + gap[0] if x >= xr: break col += 1
[docs] def tile_at (self, x, y): """Return the ``(col, row)`` tile at the point ``(x, y)``, or ``None``. Returns ``None`` within gaps between tiles. """ ts = self.tile_size gap = self.gap pos = (x, y) tile = [] for axis in (0, 1): this_tile, offset = divmod(pos[axis], float(ts[axis] + gap[axis])) if offset < ts[axis]: # in the tile tile.append(this_tile) else: # in the gap return None return tuple(tile)
[docs] def align (self, graphic, col, row, alignment=0, pad=0, offset=0): """Align a graphic or surface within a tile. align(self, graphic, col, row, alignment=0, pad=0, offset=0) -> aligned_rect ``alignment``, ``pad`` and ``offset`` are as taken by :func:`align_rect <engine.util.align_rect>`. :arg graphic: a :class:`gfx.Graphic <engine.gfx.graphic.Graphic>` instance or a Pygame surface. In the former case, the graphic is moved (but it is not cropped to fit in the tile). :arg col: column of the tile. :arg row: row of the tile. :return: a Pygame rect clipped within the tile giving the area the graphic should be put in. """ if isinstance(graphic, Graphic): rect = graphic.rect else: rect = graphic.get_rect() pos = align_rect(rect, self.tile_rect(col, row), alignment, pad, offset) if isinstance(graphic, Graphic): graphic.pos = pos return Rect(pos, rect.size)