from ._ondiskbitmap import OnDiskBitmap
from ._shape import Shape
from ._palette import Palette
-from ._structs import TransformStruct, InputPixelStruct, OutputPixelStruct
+from ._structs import (
+ InputPixelStruct,
+ OutputPixelStruct,
+ null_transform,
+)
from ._colorspace import Colorspace
from ._area import Area
class TileGrid:
- # pylint: disable=too-many-instance-attributes
+ # pylint: disable=too-many-instance-attributes, too-many-statements
"""Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
grids can share bitmaps and pixel shaders.
self._hidden_tilegrid = False
self._hidden_by_parent = False
self._rendered_hidden = False
+ self._name = "Tilegrid"
self._x = x
self._y = y
self._width_in_tiles = width
(self._width_in_tiles * self._height_in_tiles) * [default_tile]
)
self._in_group = False
- self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
+ self._absolute_transform = None
self._current_area = Area(0, 0, self._pixel_width, self._pixel_height)
self._dirty_area = Area(0, 0, 0, 0)
self._previous_area = Area(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF)
def _update_transform(self, absolute_transform):
"""Update the parent transform and child transforms"""
+ self._in_group = absolute_transform is not None
self._absolute_transform = absolute_transform
if self._absolute_transform is not None:
+ self._moved = True
self._update_current_x()
self._update_current_y()
else:
width = self._pixel_width
- if self._absolute_transform.transpose_xy:
+ absolute_transform = (
+ null_transform
+ if self._absolute_transform is None
+ else self._absolute_transform
+ )
+
+ if absolute_transform.transpose_xy:
self._current_area.y1 = (
- self._absolute_transform.y + self._absolute_transform.dy * self._x
+ absolute_transform.y + absolute_transform.dy * self._x
)
- self._current_area.y2 = (
- self._absolute_transform.y
- + self._absolute_transform.dy * (self._x + width)
+ self._current_area.y2 = absolute_transform.y + absolute_transform.dy * (
+ self._x + width
)
if self._current_area.y2 < self._current_area.y1:
self._current_area.y1, self._current_area.y2 = (
)
else:
self._current_area.x1 = (
- self._absolute_transform.x + self._absolute_transform.dx * self._x
+ absolute_transform.x + absolute_transform.dx * self._x
)
- self._current_area.x2 = (
- self._absolute_transform.x
- + self._absolute_transform.dx * (self._x + width)
+ self._current_area.x2 = absolute_transform.x + absolute_transform.dx * (
+ self._x + width
)
if self._current_area.x2 < self._current_area.x1:
self._current_area.x1, self._current_area.x2 = (
else:
height = self._pixel_height
- if self._absolute_transform.transpose_xy:
+ absolute_transform = (
+ null_transform
+ if self._absolute_transform is None
+ else self._absolute_transform
+ )
+
+ if absolute_transform.transpose_xy:
self._current_area.x1 = (
- self._absolute_transform.x + self._absolute_transform.dx * self._y
+ absolute_transform.x + absolute_transform.dx * self._y
)
- self._current_area.x2 = (
- self._absolute_transform.x
- + self._absolute_transform.dx * (self._y + height)
+ self._current_area.x2 = absolute_transform.x + absolute_transform.dx * (
+ self._y + height
)
if self._current_area.x2 < self._current_area.x1:
self._current_area.x1, self._current_area.x2 = (
)
else:
self._current_area.y1 = (
- self._absolute_transform.y + self._absolute_transform.dy * self._y
+ absolute_transform.y + absolute_transform.dy * self._y
)
- self._current_area.y2 = (
- self._absolute_transform.y
- + self._absolute_transform.dy * (self._y + height)
+ self._current_area.y2 = absolute_transform.y + absolute_transform.dy * (
+ self._y + height
)
if self._current_area.y2 < self._current_area.y1:
self._current_area.y1, self._current_area.y2 = (
# If no tiles are present we have no impact
tiles = self._tiles
- if self._hidden_tilegrid or self._hidden_by_parent:
+ if tiles is None or len(tiles) == 0:
return False
- overlap = Area()
- if not self._current_area.compute_overlap(area, overlap):
+ if self._hidden_tilegrid or self._hidden_by_parent:
+ return False
+ overlap = Area() # area, current_area, overlap
+ if not area.compute_overlap(self._current_area, overlap):
return False
+ # else:
+ # print("Checking", area.x1, area.y1, area.x2, area.y2)
+ # print("Overlap", overlap.x1, overlap.y1, overlap.x2, overlap.y2)
if self._bitmap.width <= 0 or self._bitmap.height <= 0:
return False
start += (area.y2 - area.y1 - 1) * y_stride
y_stride *= -1
+ # Track if this layer finishes filling in the given area. We can ignore any remaining
+ # layers at that point.
full_coverage = area == overlap
transformed = Area()
else:
y_shift = overlap.y1 - area.y1
+ # This untransposes x and y so it aligns with bitmap rows
if self._transpose_xy != self._absolute_transform.transpose_xy:
x_stride, y_stride = y_stride, x_stride
x_shift, y_shift = y_shift, x_shift
) # In Pixels
local_y = input_pixel.y // self._absolute_transform.scale
for input_pixel.x in range(start_x, end_x):
+ # Compute the destination pixel in the buffer and mask based on the transformations
offset = (
row_start + (input_pixel.x - start_x + x_shift) * x_stride
) # In Pixels
input_pixel.tile // self._bitmap_width_in_tiles
) * self._tile_height + local_y % self._tile_height
- input_pixel.pixel = (
- self._bitmap._get_pixel( # pylint: disable=protected-access
- input_pixel.tile_x, input_pixel.tile_y
+ output_pixel.pixel = 0
+ input_pixel.pixel = 0
+
+ # We always want to read bitmap pixels by row first and then transpose into
+ # the destination buffer because most bitmaps are row associated.
+ if isinstance(self._bitmap, (Bitmap, Shape, OnDiskBitmap)):
+ input_pixel.pixel = (
+ self._bitmap._get_pixel( # pylint: disable=protected-access
+ input_pixel.tile_x, input_pixel.tile_y
+ )
)
- )
- output_pixel.opaque = True
+ output_pixel.opaque = True
if self._pixel_shader is None:
output_pixel.pixel = input_pixel.pixel
elif isinstance(self._pixel_shader, Palette):
full_coverage = False
else:
mask[offset // 32] |= 1 << (offset % 32)
+ # print("Mask", mask)
if colorspace.depth == 16:
struct.pack_into(
"H",
- buffer,
- offset * 2,
+ buffer.cast("H"),
+ offset,
output_pixel.pixel,
)
elif colorspace.depth == 32:
struct.pack_into(
"I",
buffer,
- offset * 4,
+ offset,
output_pixel.pixel,
)
elif colorspace.depth == 8:
- buffer[offset] = output_pixel.pixel & 0xFF
+ buffer.cast("B")[offset] = output_pixel.pixel & 0xFF
elif colorspace.depth < 8:
# Reorder the offsets to pack multiple rows into
# a byte (meaning they share a column).
# even if we multiply it back out
offset = (
col * pixels_per_byte
- + (row // pixels_per_byte) * width
+ + (row // pixels_per_byte) * pixels_per_byte * width
+ (row % pixels_per_byte)
)
shift = (offset % pixels_per_byte) * colorspace.depth
if colorspace.reverse_pixels_in_byte:
# Reverse the shift by subtracting it from the leftmost shift
shift = (pixels_per_byte - 1) * colorspace.depth - shift
- buffer[offset // pixels_per_byte] |= output_pixel.pixel << shift
+ buffer.cast("B")[offset // pixels_per_byte] |= (
+ output_pixel.pixel << shift
+ )
+
return full_coverage
def _finish_refresh(self):
if isinstance(self._bitmap, Bitmap):
self._bitmap._get_refresh_areas(areas) # pylint: disable=protected-access
refresh_area = areas[-1] if areas else None
- if tail != refresh_area:
+ if refresh_area != tail:
# Special case a TileGrid that shows a full bitmap and use its
# dirty area. Copy it to ours so we can transform it.
if self._tiles_in_bitmap == 1:
def _get_rendered_hidden(self) -> bool:
return self._rendered_hidden
+ def _set_all_tiles(self, tile_index: int) -> None:
+ """Set all tiles to the given tile index"""
+ if tile_index >= self._tiles_in_bitmap:
+ raise ValueError("Tile index out of bounds")
+ self._tiles = bytearray(
+ (self._width_in_tiles * self._height_in_tiles) * [tile_index]
+ )
+ self._full_change = True
+
+ def _set_tile(self, x: int, y: int, tile_index: int) -> None:
+ self._tiles[y * self._width_in_tiles + x] = tile_index
+ temp_area = Area()
+ if not self._partial_change:
+ tile_area = self._dirty_area
+ else:
+ tile_area = temp_area
+ top_x = (x - self._top_left_x) % self._width_in_tiles
+ if top_x < 0:
+ top_x += self._width_in_tiles
+ tile_area.x1 = top_x * self._tile_width
+ tile_area.x2 = tile_area.x1 + self._tile_width
+ top_y = (y - self._top_left_y) % self._height_in_tiles
+ if top_y < 0:
+ top_y += self._height_in_tiles
+ tile_area.y1 = top_y * self._tile_height
+ tile_area.y2 = tile_area.y1 + self._tile_height
+
+ if self._partial_change:
+ self._dirty_area.union(temp_area, self._dirty_area)
+
+ self._partial_change = True
+
+ def _set_top_left(self, x: int, y: int) -> None:
+ self._top_left_x = x
+ self._top_left_y = y
+ self._full_change = True
+
@property
def hidden(self) -> bool:
"""True when the TileGrid is hidden. This may be False even
if not isinstance(value, int):
raise TypeError("X should be a integer type")
if self._x != value:
+ self._moved = True
self._x = value
- self._update_current_x()
+ if self._absolute_transform is not None:
+ self._update_current_x()
@property
def y(self) -> int:
if not isinstance(value, int):
raise TypeError("Y should be a integer type")
if self._y != value:
+ self._moved = True
self._y = value
- self._update_current_y()
+ if self._absolute_transform is not None:
+ self._update_current_y()
@property
def flip_x(self) -> bool:
raise TypeError("Flip X should be a boolean type")
if self._flip_x != value:
self._flip_x = value
+ self._full_change = True
@property
def flip_y(self) -> bool:
raise TypeError("Flip Y should be a boolean type")
if self._flip_y != value:
self._flip_y = value
+ self._full_change = True
@property
def transpose_xy(self) -> bool:
return self._transpose_xy
@transpose_xy.setter
- def transpose_xy(self, value: bool):
+ def transpose_xy(self, value: bool) -> None:
if not isinstance(value, bool):
raise TypeError("Transpose XY should be a boolean type")
if self._transpose_xy != value:
self._transpose_xy = value
+ if self._pixel_width == self._pixel_height:
+ self._full_change = True
+ return
self._update_current_x()
self._update_current_y()
+ self._moved = True
@property
def pixel_shader(self) -> Union[ColorConverter, Palette]:
)
self._pixel_shader = new_pixel_shader
+ self._full_change = True
@property
def bitmap(self) -> Union[Bitmap, OnDiskBitmap, Shape]:
raise ValueError("New bitmap must be same size as old bitmap")
self._bitmap = new_bitmap
+ self._full_change = True
def _extract_and_check_index(self, index):
if isinstance(index, (tuple, list)):
or index >= len(self._tiles)
):
raise ValueError("Tile index out of bounds")
- return index
+ return x, y
def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
"""Returns the tile index at the given index. The index can either be
an x,y tuple or an int equal to ``y * width + x``'.
"""
- index = self._extract_and_check_index(index)
- return self._tiles[index]
+ x, y = self._extract_and_check_index(index)
+ return self._tiles[y * self._width_in_tiles + x]
def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
"""Sets the tile index at the given index. The index can either be
an x,y tuple or an int equal to ``y * width + x``.
"""
- index = self._extract_and_check_index(index)
+ x, y = self._extract_and_check_index(index)
if not 0 <= value <= 255:
raise ValueError("Tile value out of bounds")
- self._tiles[index] = value
+ self._set_tile(x, y, value)
@property
def width(self) -> int: