X-Git-Url: https://git.ayoreis.com/hackapet/Adafruit_Blinka_Displayio.git/blobdiff_plain/85cea22ff75765e3a8fd1b00824f88fc5a3be8da..a53c930973a24a7c55db9448e45d7ca7974cb56e:/displayio/_tilegrid.py diff --git a/displayio/_tilegrid.py b/displayio/_tilegrid.py index 35f5ac1..2ee8d54 100644 --- a/displayio/_tilegrid.py +++ b/displayio/_tilegrid.py @@ -17,16 +17,19 @@ displayio for Blinka """ +import struct from typing import Union, Optional, Tuple -from PIL import Image +from circuitpython_typing import WriteableBuffer from ._bitmap import Bitmap from ._colorconverter import ColorConverter from ._ondiskbitmap import OnDiskBitmap from ._shape import Shape from ._palette import Palette -from ._structs import RectangleStruct, TransformStruct +from ._structs import TransformStruct, InputPixelStruct, OutputPixelStruct +from ._colorspace import Colorspace +from ._area import Area -__version__ = "0.0.0-auto.0" +__version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" @@ -71,10 +74,12 @@ class TileGrid: if isinstance(self._pixel_shader, ColorConverter): self._pixel_shader._rgba = True # pylint: disable=protected-access self._hidden_tilegrid = False + self._hidden_by_parent = False + self._rendered_hidden = False self._x = x self._y = y - self._width = width # Number of Tiles Wide - self._height = height # Number of Tiles High + self._width_in_tiles = width + self._height_in_tiles = height self._transpose_xy = False self._flip_x = False self._flip_y = False @@ -96,13 +101,21 @@ class TileGrid: raise ValueError("Default Tile is out of range") self._pixel_width = width * tile_width self._pixel_height = height * tile_height - self._tiles = (self._width * self._height) * [default_tile] + self._tiles = bytearray( + (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._current_area = RectangleStruct( - 0, 0, self._pixel_width, self._pixel_height - ) + 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) self._moved = False + self._full_change = True + self._partial_change = True + self._bitmap_width_in_tiles = bitmap_width // tile_width + self._tiles_in_bitmap = self._bitmap_width_in_tiles * ( + bitmap_height // tile_height + ) def _update_transform(self, absolute_transform): """Update the parent transform and child transforms""" @@ -116,6 +129,7 @@ class TileGrid: width = self._pixel_height else: width = self._pixel_width + if self._absolute_transform.transpose_xy: self._current_area.y1 = ( self._absolute_transform.y + self._absolute_transform.dy * self._x @@ -148,6 +162,7 @@ class TileGrid: height = self._pixel_width else: height = self._pixel_height + if self._absolute_transform.transpose_xy: self._current_area.x1 = ( self._absolute_transform.x + self._absolute_transform.dx * self._y @@ -196,107 +211,286 @@ class TileGrid: ) image.putalpha(alpha.convert("L")) - def _fill_area(self, buffer): - # pylint: disable=too-many-locals,too-many-branches,too-many-statements + def _fill_area( + self, + colorspace: Colorspace, + area: Area, + mask: WriteableBuffer, + buffer: WriteableBuffer, + ) -> bool: """Draw onto the image""" - if self._hidden_tilegrid: - return - - if self._bitmap.width <= 0 or self._bitmap.height <= 0: - return + # pylint: disable=too-many-locals,too-many-branches,too-many-statements - # Copy class variables to local variables in case something changes - x = self._x - y = self._y - width = self._width - height = self._height - tile_width = self._tile_width - tile_height = self._tile_height - bitmap_width = self._bitmap.width - pixel_width = self._pixel_width - pixel_height = self._pixel_height - tiles = self._tiles - absolute_transform = self._absolute_transform - pixel_shader = self._pixel_shader - bitmap = self._bitmap + # If no tiles are present we have no impact tiles = self._tiles - tile_count_x = bitmap_width // tile_width + if self._hidden_tilegrid or self._hidden_by_parent: + return False - image = Image.new( - "RGBA", - (width * tile_width, height * tile_height), - (0, 0, 0, 0), + overlap = Area() + if not self._current_area.compute_overlap(area, overlap): + return False + + if self._bitmap.width <= 0 or self._bitmap.height <= 0: + return False + + x_stride = 1 + y_stride = area.width() + + flip_x = self._flip_x + flip_y = self._flip_y + if self._transpose_xy != self._absolute_transform.transpose_xy: + flip_x, flip_y = flip_y, flip_x + + start = 0 + if (self._absolute_transform.dx < 0) != flip_x: + start += (area.x2 - area.x1 - 1) * x_stride + x_stride *= -1 + if (self._absolute_transform.dy < 0) != flip_y: + start += (area.y2 - area.y1 - 1) * y_stride + y_stride *= -1 + + full_coverage = area == overlap + + transformed = Area() + area.transform_within( + flip_x != (self._absolute_transform.dx < 0), + flip_y != (self._absolute_transform.dy < 0), + self.transpose_xy != self._absolute_transform.transpose_xy, + overlap, + self._current_area, + transformed, ) - for tile_x in range(width): - for tile_y in range(height): - tile_index = tiles[tile_y * width + tile_x] - tile_index_x = tile_index % tile_count_x - tile_index_y = tile_index // tile_count_x - tile_image = bitmap._image # pylint: disable=protected-access - if isinstance(pixel_shader, Palette): - tile_image = tile_image.copy().convert("P") - self._apply_palette(tile_image) - tile_image = tile_image.convert("RGBA") - self._add_alpha(tile_image) - elif isinstance(pixel_shader, ColorConverter): - # This will be needed for eInks, grayscale, and monochrome displays - pass - image.alpha_composite( - tile_image, - dest=(tile_x * tile_width, tile_y * tile_height), - source=( - tile_index_x * tile_width, - tile_index_y * tile_height, - tile_index_x * tile_width + tile_width, - tile_index_y * tile_height + tile_height, - ), + start_x = transformed.x1 - self._current_area.x1 + end_x = transformed.x2 - self._current_area.x1 + start_y = transformed.y1 - self._current_area.y1 + end_y = transformed.y2 - self._current_area.y1 + + if (self._absolute_transform.dx < 0) != flip_x: + x_shift = area.x2 - overlap.x2 + else: + x_shift = overlap.x1 - area.x1 + if (self._absolute_transform.dy < 0) != flip_y: + y_shift = area.y2 - overlap.y2 + else: + y_shift = overlap.y1 - area.y1 + + 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 + + pixels_per_byte = 8 // colorspace.depth + + input_pixel = InputPixelStruct() + output_pixel = OutputPixelStruct() + for input_pixel.y in range(start_y, end_y): + row_start = ( + start + (input_pixel.y - start_y + y_shift) * y_stride + ) # In Pixels + local_y = input_pixel.y // self._absolute_transform.scale + for input_pixel.x in range(start_x, end_x): + offset = ( + row_start + (input_pixel.x - start_x + x_shift) * x_stride + ) # In Pixels + + # Check the mask first to see if the pixel has already been set + if mask[offset // 32] & (1 << (offset % 32)): + continue + local_x = input_pixel.x // self._absolute_transform.scale + tile_location = ( + (local_y // self._tile_height + self._top_left_y) + % self._height_in_tiles + ) * self._width_in_tiles + ( + local_x // self._tile_width + self._top_left_x + ) % self._width_in_tiles + input_pixel.tile = tiles[tile_location] + input_pixel.tile_x = ( + input_pixel.tile % self._bitmap_width_in_tiles + ) * self._tile_width + local_x % self._tile_width + input_pixel.tile_y = ( + 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.opaque = True + + if self._pixel_shader is None: + output_pixel.pixel = input_pixel.pixel + elif isinstance(self._pixel_shader, Palette): + self._pixel_shader._get_color( # pylint: disable=protected-access + colorspace, input_pixel, output_pixel + ) + elif isinstance(self._pixel_shader, ColorConverter): + self._pixel_shader._convert( # pylint: disable=protected-access + colorspace, input_pixel, output_pixel + ) + + if not output_pixel.opaque: + full_coverage = False + else: + mask[offset // 32] |= 1 << (offset % 32) + if colorspace.depth == 16: + struct.pack_into( + "H", + buffer, + offset * 2, + output_pixel.pixel, + ) + elif colorspace.depth == 32: + struct.pack_into( + "I", + buffer, + offset * 4, + output_pixel.pixel, + ) + elif colorspace.depth == 8: + buffer[offset] = output_pixel.pixel & 0xFF + elif colorspace.depth < 8: + # Reorder the offsets to pack multiple rows into + # a byte (meaning they share a column). + if not colorspace.pixels_in_byte_share_row: + width = area.width() + row = offset // width + col = offset % width + # Dividing by pixels_per_byte does truncated division + # even if we multiply it back out + offset = ( + col * pixels_per_byte + + (row // 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 + return full_coverage + + def _finish_refresh(self): + first_draw = self._previous_area.x1 == self._previous_area.x2 + hidden = self._hidden_tilegrid or self._hidden_by_parent + if not first_draw and hidden: + self._previous_area.x2 = self._previous_area.x1 + elif self._moved or first_draw: + self._current_area.copy_into(self._previous_area) + + self._moved = False + self._full_change = False + self._partial_change = False + if isinstance(self._pixel_shader, (Palette, ColorConverter)): + self._pixel_shader._finish_refresh() # pylint: disable=protected-access + if isinstance(self._bitmap, (Bitmap, Shape)): + self._bitmap._finish_refresh() # pylint: disable=protected-access + + def _get_refresh_areas(self, areas: list[Area]) -> None: + # pylint: disable=invalid-name, too-many-branches, too-many-statements + first_draw = self._previous_area.x1 == self._previous_area.x2 + hidden = self._hidden_tilegrid or self._hidden_by_parent + + # Check hidden first because it trumps all other changes + if hidden: + self._rendered_hidden = True + if not first_draw: + areas.append(self._previous_area) + return + if self._moved and not first_draw: + self._previous_area.union(self._current_area, self._dirty_area) + if self._dirty_area.size() < 2 * self._pixel_width * self._pixel_height: + areas.append(self._dirty_area) + return + areas.append(self._current_area) + areas.append(self._previous_area) + return + + tail = areas[-1] if areas else None + # If we have an in-memory bitmap, then check it for modifications + 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: + # 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: + refresh_area.copy_into(self._dirty_area) + self._partial_change = True + else: + self._full_change = True + elif isinstance(self._bitmap, Shape): + self._bitmap._get_refresh_areas(areas) # pylint: disable=protected-access + refresh_area = areas[-1] if areas else None + if refresh_area != tail: + refresh_area.copy_into(self._dirty_area) + self._partial_change = True + + self._full_change = self._full_change or ( + isinstance(self._pixel_shader, (Palette, ColorConverter)) + and self._pixel_shader._needs_refresh # pylint: disable=protected-access + ) + if self._full_change or first_draw: + areas.append(self._current_area) + return - if absolute_transform is not None: - if absolute_transform.scale > 1: - image = image.resize( - ( - int(pixel_width * absolute_transform.scale), - int( - pixel_height * absolute_transform.scale, - ), - ), - resample=Image.NEAREST, + if self._partial_change: + x = self._x + y = self._y + if self._absolute_transform.transpose_xy: + x, y = y, x + x1 = self._dirty_area.x1 + x2 = self._dirty_area.x2 + if self._flip_x: + x1 = self._pixel_width - x1 + x2 = self._pixel_width - x2 + y1 = self._dirty_area.y1 + y2 = self._dirty_area.y2 + if self._flip_y: + y1 = self._pixel_height - y1 + y2 = self._pixel_height - y2 + if self._transpose_xy != self._absolute_transform.transpose_xy: + x1, y1 = y1, x1 + x2, y2 = y2, x2 + self._dirty_area.x1 = ( + self._absolute_transform.x + self._absolute_transform.dx * (x + x1) + ) + self._dirty_area.y1 = ( + self._absolute_transform.y + self._absolute_transform.dy * (y + y1) + ) + self._dirty_area.x2 = ( + self._absolute_transform.x + self._absolute_transform.dx * (x + x2) + ) + self._dirty_area.y2 = ( + self._absolute_transform.y + self._absolute_transform.dy * (y + y2) + ) + if self._dirty_area.y2 < self._dirty_area.y1: + self._dirty_area.y1, self._dirty_area.y2 = ( + self._dirty_area.y2, + self._dirty_area.y1, + ) + if self._dirty_area.x2 < self._dirty_area.x1: + self._dirty_area.x1, self._dirty_area.x2 = ( + self._dirty_area.x2, + self._dirty_area.x1, ) - if absolute_transform.mirror_x: - image = image.transpose(Image.FLIP_LEFT_RIGHT) - if absolute_transform.mirror_y: - image = image.transpose(Image.FLIP_TOP_BOTTOM) - if absolute_transform.transpose_xy: - image = image.transpose(Image.TRANSPOSE) - x *= absolute_transform.dx - y *= absolute_transform.dy - x += absolute_transform.x - y += absolute_transform.y - - source_x = source_y = 0 - if x < 0: - source_x = round(0 - x) - x = 0 - if y < 0: - source_y = round(0 - y) - y = 0 - - x = round(x) - y = round(y) + areas.append(self._dirty_area) - if ( - x <= buffer.width - and y <= buffer.height - and source_x <= image.width - and source_y <= image.height - ): - buffer.alpha_composite(image, (x, y), source=(source_x, source_y)) + def _set_hidden(self, hidden: bool) -> None: + self._hidden_tilegrid = hidden + self._rendered_hidden = False + if not hidden: + self._full_change = True - def _finish_refresh(self): - pass + def _set_hidden_by_parent(self, hidden: bool) -> None: + self._hidden_by_parent = hidden + self._rendered_hidden = False + if not hidden: + self._full_change = True + + def _get_rendered_hidden(self) -> bool: + return self._rendered_hidden @property def hidden(self) -> bool: @@ -308,7 +502,8 @@ class TileGrid: def hidden(self, value: bool): if not isinstance(value, (bool, int)): raise ValueError("Expecting a boolean or integer value") - self._hidden_tilegrid = bool(value) + value = bool(value) + self._set_hidden(value) @property def x(self) -> int: @@ -381,15 +576,54 @@ class TileGrid: """The pixel shader of the tilegrid.""" return self._pixel_shader + @pixel_shader.setter + def pixel_shader(self, new_pixel_shader: Union[ColorConverter, Palette]) -> None: + if not isinstance(new_pixel_shader, ColorConverter) and not isinstance( + new_pixel_shader, Palette + ): + raise TypeError( + "Unsupported Type: new_pixel_shader must be ColorConverter or Palette" + ) + + self._pixel_shader = new_pixel_shader + + @property + def bitmap(self) -> Union[Bitmap, OnDiskBitmap, Shape]: + """The Bitmap, OnDiskBitmap, or Shape that is assigned to this TileGrid""" + return self._bitmap + + @bitmap.setter + def bitmap(self, new_bitmap: Union[Bitmap, OnDiskBitmap, Shape]) -> None: + if ( + not isinstance(new_bitmap, Bitmap) + and not isinstance(new_bitmap, OnDiskBitmap) + and not isinstance(new_bitmap, Shape) + ): + raise TypeError( + "Unsupported Type: new_bitmap must be Bitmap, OnDiskBitmap, or Shape" + ) + + if ( + new_bitmap.width != self.bitmap.width + or new_bitmap.height != self.bitmap.height + ): + raise ValueError("New bitmap must be same size as old bitmap") + + self._bitmap = new_bitmap + def _extract_and_check_index(self, index): if isinstance(index, (tuple, list)): x = index[0] y = index[1] - index = y * self._width + x + index = y * self._width_in_tiles + x elif isinstance(index, int): - x = index % self._width - y = index // self._width - if x > self._width or y > self._height or index >= len(self._tiles): + x = index % self._width_in_tiles + y = index // self._width_in_tiles + if ( + x > self._width_in_tiles + or y > self._height_in_tiles + or index >= len(self._tiles) + ): raise ValueError("Tile index out of bounds") return index @@ -410,21 +644,21 @@ class TileGrid: self._tiles[index] = value @property - def width(self): + def width(self) -> int: """Width in tiles""" - return self._width + return self._width_in_tiles @property - def height(self): + def height(self) -> int: """Height in tiles""" - return self._height + return self._height_in_tiles @property - def tile_width(self): + def tile_width(self) -> int: """Width of each tile in pixels""" return self._tile_width @property - def tile_height(self): + def tile_height(self) -> int: """Height of each tile in pixels""" return self._tile_height