]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_tilegrid.py
87d9a921e7348508337b2368625971f64bc6b86a
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _tilegrid.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `displayio.tilegrid`
7 ================================================================================
8
9 displayio for Blinka
10
11 **Software and Dependencies:**
12
13 * Adafruit Blinka:
14   https://github.com/adafruit/Adafruit_Blinka/releases
15
16 * Author(s): Melissa LeBlanc-Williams
17
18 """
19
20 import struct
21 from typing import Union, Optional, Tuple
22 from circuitpython_typing import WriteableBuffer
23 from ._bitmap import Bitmap
24 from ._colorconverter import ColorConverter
25 from ._ondiskbitmap import OnDiskBitmap
26 from ._shape import Shape
27 from ._palette import Palette
28 from ._structs import (
29     InputPixelStruct,
30     OutputPixelStruct,
31     null_transform,
32 )
33 from ._colorspace import Colorspace
34 from ._area import Area
35
36 __version__ = "0.0.0+auto.0"
37 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
38
39
40 class TileGrid:
41     # pylint: disable=too-many-instance-attributes, too-many-statements
42     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
43     grids can share bitmaps and pixel shaders.
44
45     A single tile grid is also known as a Sprite.
46     """
47
48     def __init__(
49         self,
50         bitmap: Union[Bitmap, OnDiskBitmap, Shape],
51         *,
52         pixel_shader: Union[ColorConverter, Palette],
53         width: int = 1,
54         height: int = 1,
55         tile_width: Optional[int] = None,
56         tile_height: Optional[int] = None,
57         default_tile: int = 0,
58         x: int = 0,
59         y: int = 0,
60     ):
61         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
62         used to convert the value and its location to a display native pixel color. This may
63         be a simple color palette lookup, a gradient, a pattern or a color transformer.
64
65         tile_width and tile_height match the height of the bitmap by default.
66         """
67         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
68             raise ValueError("Unsupported Bitmap type")
69         self._bitmap = bitmap
70         bitmap_width = bitmap.width
71         bitmap_height = bitmap.height
72
73         if pixel_shader is not None and not isinstance(
74             pixel_shader, (ColorConverter, Palette)
75         ):
76             raise ValueError("Unsupported Pixel Shader type")
77         self._pixel_shader = pixel_shader
78         if isinstance(self._pixel_shader, ColorConverter):
79             self._pixel_shader._rgba = True  # pylint: disable=protected-access
80         self._hidden_tilegrid = False
81         self._hidden_by_parent = False
82         self._rendered_hidden = False
83         self._name = "Tilegrid"
84         self._x = x
85         self._y = y
86         self._width_in_tiles = width
87         self._height_in_tiles = height
88         self._transpose_xy = False
89         self._flip_x = False
90         self._flip_y = False
91         self._top_left_x = 0
92         self._top_left_y = 0
93         if tile_width is None or tile_width == 0:
94             tile_width = bitmap_width
95         if tile_height is None or tile_width == 0:
96             tile_height = bitmap_height
97         if tile_width < 1 or tile_height < 1:
98             raise ValueError("Tile width and height must be greater than 0")
99         if bitmap_width % tile_width != 0:
100             raise ValueError("Tile width must exactly divide bitmap width")
101         self._tile_width = tile_width
102         if bitmap_height % tile_height != 0:
103             raise ValueError("Tile height must exactly divide bitmap height")
104         self._tile_height = tile_height
105         if not 0 <= default_tile <= 255:
106             raise ValueError("Default Tile is out of range")
107         self._pixel_width = width * tile_width
108         self._pixel_height = height * tile_height
109         self._tiles = bytearray(
110             (self._width_in_tiles * self._height_in_tiles) * [default_tile]
111         )
112         self._in_group = False
113         self._absolute_transform = None
114         self._current_area = Area(0, 0, self._pixel_width, self._pixel_height)
115         self._dirty_area = Area(0, 0, 0, 0)
116         self._previous_area = Area(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF)
117         self._moved = False
118         self._full_change = True
119         self._partial_change = True
120         self._bitmap_width_in_tiles = bitmap_width // tile_width
121         self._tiles_in_bitmap = self._bitmap_width_in_tiles * (
122             bitmap_height // tile_height
123         )
124
125     def _update_transform(self, absolute_transform):
126         """Update the parent transform and child transforms"""
127         self._in_group = absolute_transform is not None
128         self._absolute_transform = absolute_transform
129         if self._absolute_transform is not None:
130             self._moved = True
131             self._update_current_x()
132             self._update_current_y()
133
134     def _update_current_x(self):
135         if self._transpose_xy:
136             width = self._pixel_height
137         else:
138             width = self._pixel_width
139
140         absolute_transform = (
141             null_transform
142             if self._absolute_transform is None
143             else self._absolute_transform
144         )
145
146         if absolute_transform.transpose_xy:
147             self._current_area.y1 = (
148                 absolute_transform.y + absolute_transform.dy * self._x
149             )
150             self._current_area.y2 = absolute_transform.y + absolute_transform.dy * (
151                 self._x + width
152             )
153             if self._current_area.y2 < self._current_area.y1:
154                 self._current_area.y1, self._current_area.y2 = (
155                     self._current_area.y2,
156                     self._current_area.y1,
157                 )
158         else:
159             self._current_area.x1 = (
160                 absolute_transform.x + absolute_transform.dx * self._x
161             )
162             self._current_area.x2 = absolute_transform.x + absolute_transform.dx * (
163                 self._x + width
164             )
165             if self._current_area.x2 < self._current_area.x1:
166                 self._current_area.x1, self._current_area.x2 = (
167                     self._current_area.x2,
168                     self._current_area.x1,
169                 )
170
171     def _update_current_y(self):
172         if self._transpose_xy:
173             height = self._pixel_width
174         else:
175             height = self._pixel_height
176
177         absolute_transform = (
178             null_transform
179             if self._absolute_transform is None
180             else self._absolute_transform
181         )
182
183         if absolute_transform.transpose_xy:
184             self._current_area.x1 = (
185                 absolute_transform.x + absolute_transform.dx * self._y
186             )
187             self._current_area.x2 = absolute_transform.x + absolute_transform.dx * (
188                 self._y + height
189             )
190             if self._current_area.x2 < self._current_area.x1:
191                 self._current_area.x1, self._current_area.x2 = (
192                     self._current_area.x2,
193                     self._current_area.x1,
194                 )
195         else:
196             self._current_area.y1 = (
197                 absolute_transform.y + absolute_transform.dy * self._y
198             )
199             self._current_area.y2 = absolute_transform.y + absolute_transform.dy * (
200                 self._y + height
201             )
202             if self._current_area.y2 < self._current_area.y1:
203                 self._current_area.y1, self._current_area.y2 = (
204                     self._current_area.y2,
205                     self._current_area.y1,
206                 )
207
208     def _shade(self, pixel_value):
209         if isinstance(self._pixel_shader, Palette):
210             return self._pixel_shader[pixel_value]
211         if isinstance(self._pixel_shader, ColorConverter):
212             return self._pixel_shader.convert(pixel_value)
213         return pixel_value
214
215     def _apply_palette(self, image):
216         image.putpalette(
217             self._pixel_shader._get_palette()  # pylint: disable=protected-access
218         )
219
220     def _add_alpha(self, image):
221         alpha = self._bitmap._image.copy().convert(  # pylint: disable=protected-access
222             "P"
223         )
224         alpha.putpalette(
225             self._pixel_shader._get_alpha_palette()  # pylint: disable=protected-access
226         )
227         image.putalpha(alpha.convert("L"))
228
229     def _fill_area(
230         self,
231         colorspace: Colorspace,
232         area: Area,
233         mask: WriteableBuffer,
234         buffer: WriteableBuffer,
235     ) -> bool:
236         """Draw onto the image"""
237         # pylint: disable=too-many-locals,too-many-branches,too-many-statements
238
239         # If no tiles are present we have no impact
240         tiles = self._tiles
241
242         if tiles is None or len(tiles) == 0:
243             return False
244
245         if self._hidden_tilegrid or self._hidden_by_parent:
246             return False
247         overlap = Area()  # area, current_area, overlap
248         if not area.compute_overlap(self._current_area, overlap):
249             return False
250         # else:
251         #    print("Checking", area.x1, area.y1, area.x2, area.y2)
252         #    print("Overlap", overlap.x1, overlap.y1, overlap.x2, overlap.y2)
253
254         if self._bitmap.width <= 0 or self._bitmap.height <= 0:
255             return False
256
257         x_stride = 1
258         y_stride = area.width()
259
260         flip_x = self._flip_x
261         flip_y = self._flip_y
262         if self._transpose_xy != self._absolute_transform.transpose_xy:
263             flip_x, flip_y = flip_y, flip_x
264
265         start = 0
266         if (self._absolute_transform.dx < 0) != flip_x:
267             start += (area.x2 - area.x1 - 1) * x_stride
268             x_stride *= -1
269         if (self._absolute_transform.dy < 0) != flip_y:
270             start += (area.y2 - area.y1 - 1) * y_stride
271             y_stride *= -1
272
273         # Track if this layer finishes filling in the given area. We can ignore any remaining
274         # layers at that point.
275         full_coverage = area == overlap
276
277         transformed = Area()
278         area.transform_within(
279             flip_x != (self._absolute_transform.dx < 0),
280             flip_y != (self._absolute_transform.dy < 0),
281             self.transpose_xy != self._absolute_transform.transpose_xy,
282             overlap,
283             self._current_area,
284             transformed,
285         )
286
287         start_x = transformed.x1 - self._current_area.x1
288         end_x = transformed.x2 - self._current_area.x1
289         start_y = transformed.y1 - self._current_area.y1
290         end_y = transformed.y2 - self._current_area.y1
291
292         if (self._absolute_transform.dx < 0) != flip_x:
293             x_shift = area.x2 - overlap.x2
294         else:
295             x_shift = overlap.x1 - area.x1
296         if (self._absolute_transform.dy < 0) != flip_y:
297             y_shift = area.y2 - overlap.y2
298         else:
299             y_shift = overlap.y1 - area.y1
300
301         # This untransposes x and y so it aligns with bitmap rows
302         if self._transpose_xy != self._absolute_transform.transpose_xy:
303             x_stride, y_stride = y_stride, x_stride
304             x_shift, y_shift = y_shift, x_shift
305
306         pixels_per_byte = 8 // colorspace.depth
307
308         input_pixel = InputPixelStruct()
309         output_pixel = OutputPixelStruct()
310         for input_pixel.y in range(start_y, end_y):
311             row_start = (
312                 start + (input_pixel.y - start_y + y_shift) * y_stride
313             )  # In Pixels
314             local_y = input_pixel.y // self._absolute_transform.scale
315             for input_pixel.x in range(start_x, end_x):
316                 # Compute the destination pixel in the buffer and mask based on the transformations
317                 offset = (
318                     row_start + (input_pixel.x - start_x + x_shift) * x_stride
319                 )  # In Pixels
320
321                 # Check the mask first to see if the pixel has already been set
322                 if mask[offset // 32] & (1 << (offset % 32)):
323                     continue
324                 local_x = input_pixel.x // self._absolute_transform.scale
325                 tile_location = (
326                     (local_y // self._tile_height + self._top_left_y)
327                     % self._height_in_tiles
328                 ) * self._width_in_tiles + (
329                     local_x // self._tile_width + self._top_left_x
330                 ) % self._width_in_tiles
331                 input_pixel.tile = tiles[tile_location]
332                 input_pixel.tile_x = (
333                     input_pixel.tile % self._bitmap_width_in_tiles
334                 ) * self._tile_width + local_x % self._tile_width
335                 input_pixel.tile_y = (
336                     input_pixel.tile // self._bitmap_width_in_tiles
337                 ) * self._tile_height + local_y % self._tile_height
338
339                 output_pixel.pixel = 0
340                 input_pixel.pixel = 0
341
342                 # We always want to read bitmap pixels by row first and then transpose into
343                 # the destination buffer because most bitmaps are row associated.
344                 if isinstance(self._bitmap, (Bitmap, Shape, OnDiskBitmap)):
345                     input_pixel.pixel = (
346                         self._bitmap._get_pixel(  # pylint: disable=protected-access
347                             input_pixel.tile_x, input_pixel.tile_y
348                         )
349                     )
350
351                 output_pixel.opaque = True
352                 if self._pixel_shader is None:
353                     output_pixel.pixel = input_pixel.pixel
354                 elif isinstance(self._pixel_shader, Palette):
355                     self._pixel_shader._get_color(  # pylint: disable=protected-access
356                         colorspace, input_pixel, output_pixel
357                     )
358                 elif isinstance(self._pixel_shader, ColorConverter):
359                     self._pixel_shader._convert(  # pylint: disable=protected-access
360                         colorspace, input_pixel, output_pixel
361                     )
362
363                 if not output_pixel.opaque:
364                     full_coverage = False
365                 else:
366                     mask[offset // 32] |= 1 << (offset % 32)
367                     if colorspace.depth == 16:
368                         struct.pack_into(
369                             "H",
370                             buffer.cast("B"),
371                             offset * 2,
372                             output_pixel.pixel,
373                         )
374                     elif colorspace.depth == 32:
375                         struct.pack_into(
376                             "I",
377                             buffer.cast("B"),
378                             offset * 4,
379                             output_pixel.pixel,
380                         )
381                     elif colorspace.depth == 8:
382                         buffer.cast("B")[offset] = output_pixel.pixel & 0xFF
383                     elif colorspace.depth < 8:
384                         # Reorder the offsets to pack multiple rows into
385                         # a byte (meaning they share a column).
386                         if not colorspace.pixels_in_byte_share_row:
387                             width = area.width()
388                             row = offset // width
389                             col = offset % width
390                             # Dividing by pixels_per_byte does truncated division
391                             # even if we multiply it back out
392                             offset = (
393                                 col * pixels_per_byte
394                                 + (row // pixels_per_byte) * pixels_per_byte * width
395                                 + (row % pixels_per_byte)
396                             )
397                         shift = (offset % pixels_per_byte) * colorspace.depth
398                         if colorspace.reverse_pixels_in_byte:
399                             # Reverse the shift by subtracting it from the leftmost shift
400                             shift = (pixels_per_byte - 1) * colorspace.depth - shift
401                         buffer.cast("B")[offset // pixels_per_byte] |= (
402                             output_pixel.pixel << shift
403                         )
404
405         return full_coverage
406
407     def _finish_refresh(self):
408         first_draw = self._previous_area.x1 == self._previous_area.x2
409         hidden = self._hidden_tilegrid or self._hidden_by_parent
410         if not first_draw and hidden:
411             self._previous_area.x2 = self._previous_area.x1
412         elif self._moved or first_draw:
413             self._current_area.copy_into(self._previous_area)
414
415         self._moved = False
416         self._full_change = False
417         self._partial_change = False
418         if isinstance(self._pixel_shader, (Palette, ColorConverter)):
419             self._pixel_shader._finish_refresh()  # pylint: disable=protected-access
420         if isinstance(self._bitmap, (Bitmap, Shape)):
421             self._bitmap._finish_refresh()  # pylint: disable=protected-access
422
423     def _get_refresh_areas(self, areas: list[Area]) -> None:
424         # pylint: disable=invalid-name, too-many-branches, too-many-statements
425         first_draw = self._previous_area.x1 == self._previous_area.x2
426         hidden = self._hidden_tilegrid or self._hidden_by_parent
427
428         # Check hidden first because it trumps all other changes
429         if hidden:
430             self._rendered_hidden = True
431             if not first_draw:
432                 areas.append(self._previous_area)
433             return
434         if self._moved and not first_draw:
435             self._previous_area.union(self._current_area, self._dirty_area)
436             if self._dirty_area.size() < 2 * self._pixel_width * self._pixel_height:
437                 areas.append(self._dirty_area)
438                 return
439             areas.append(self._current_area)
440             areas.append(self._previous_area)
441             return
442
443         tail = areas[-1] if areas else None
444         # If we have an in-memory bitmap, then check it for modifications
445         if isinstance(self._bitmap, Bitmap):
446             self._bitmap._get_refresh_areas(areas)  # pylint: disable=protected-access
447             refresh_area = areas[-1] if areas else None
448             if refresh_area != tail:
449                 # Special case a TileGrid that shows a full bitmap and use its
450                 # dirty area. Copy it to ours so we can transform it.
451                 if self._tiles_in_bitmap == 1:
452                     refresh_area.copy_into(self._dirty_area)
453                     self._partial_change = True
454                 else:
455                     self._full_change = True
456         elif isinstance(self._bitmap, Shape):
457             self._bitmap._get_refresh_areas(areas)  # pylint: disable=protected-access
458             refresh_area = areas[-1] if areas else None
459             if refresh_area != tail:
460                 refresh_area.copy_into(self._dirty_area)
461                 self._partial_change = True
462
463         self._full_change = self._full_change or (
464             isinstance(self._pixel_shader, (Palette, ColorConverter))
465             and self._pixel_shader._needs_refresh  # pylint: disable=protected-access
466         )
467         if self._full_change or first_draw:
468             areas.append(self._current_area)
469             return
470
471         if self._partial_change:
472             x = self._x
473             y = self._y
474             if self._absolute_transform.transpose_xy:
475                 x, y = y, x
476             x1 = self._dirty_area.x1
477             x2 = self._dirty_area.x2
478             if self._flip_x:
479                 x1 = self._pixel_width - x1
480                 x2 = self._pixel_width - x2
481             y1 = self._dirty_area.y1
482             y2 = self._dirty_area.y2
483             if self._flip_y:
484                 y1 = self._pixel_height - y1
485                 y2 = self._pixel_height - y2
486             if self._transpose_xy != self._absolute_transform.transpose_xy:
487                 x1, y1 = y1, x1
488                 x2, y2 = y2, x2
489             self._dirty_area.x1 = (
490                 self._absolute_transform.x + self._absolute_transform.dx * (x + x1)
491             )
492             self._dirty_area.y1 = (
493                 self._absolute_transform.y + self._absolute_transform.dy * (y + y1)
494             )
495             self._dirty_area.x2 = (
496                 self._absolute_transform.x + self._absolute_transform.dx * (x + x2)
497             )
498             self._dirty_area.y2 = (
499                 self._absolute_transform.y + self._absolute_transform.dy * (y + y2)
500             )
501             if self._dirty_area.y2 < self._dirty_area.y1:
502                 self._dirty_area.y1, self._dirty_area.y2 = (
503                     self._dirty_area.y2,
504                     self._dirty_area.y1,
505                 )
506             if self._dirty_area.x2 < self._dirty_area.x1:
507                 self._dirty_area.x1, self._dirty_area.x2 = (
508                     self._dirty_area.x2,
509                     self._dirty_area.x1,
510                 )
511             areas.append(self._dirty_area)
512
513     def _set_hidden(self, hidden: bool) -> None:
514         self._hidden_tilegrid = hidden
515         self._rendered_hidden = False
516         if not hidden:
517             self._full_change = True
518
519     def _set_hidden_by_parent(self, hidden: bool) -> None:
520         self._hidden_by_parent = hidden
521         self._rendered_hidden = False
522         if not hidden:
523             self._full_change = True
524
525     def _get_rendered_hidden(self) -> bool:
526         return self._rendered_hidden
527
528     def _set_all_tiles(self, tile_index: int) -> None:
529         """Set all tiles to the given tile index"""
530         if tile_index >= self._tiles_in_bitmap:
531             raise ValueError("Tile index out of bounds")
532         self._tiles = bytearray(
533             (self._width_in_tiles * self._height_in_tiles) * [tile_index]
534         )
535         self._full_change = True
536
537     def _set_tile(self, x: int, y: int, tile_index: int) -> None:
538         self._tiles[y * self._width_in_tiles + x] = tile_index
539         temp_area = Area()
540         if not self._partial_change:
541             tile_area = self._dirty_area
542         else:
543             tile_area = temp_area
544         top_x = (x - self._top_left_x) % self._width_in_tiles
545         if top_x < 0:
546             top_x += self._width_in_tiles
547         tile_area.x1 = top_x * self._tile_width
548         tile_area.x2 = tile_area.x1 + self._tile_width
549         top_y = (y - self._top_left_y) % self._height_in_tiles
550         if top_y < 0:
551             top_y += self._height_in_tiles
552         tile_area.y1 = top_y * self._tile_height
553         tile_area.y2 = tile_area.y1 + self._tile_height
554
555         if self._partial_change:
556             self._dirty_area.union(temp_area, self._dirty_area)
557
558         self._partial_change = True
559
560     def _set_top_left(self, x: int, y: int) -> None:
561         self._top_left_x = x
562         self._top_left_y = y
563         self._full_change = True
564
565     @property
566     def hidden(self) -> bool:
567         """True when the TileGrid is hidden. This may be False even
568         when a part of a hidden Group."""
569         return self._hidden_tilegrid
570
571     @hidden.setter
572     def hidden(self, value: bool):
573         if not isinstance(value, (bool, int)):
574             raise ValueError("Expecting a boolean or integer value")
575         value = bool(value)
576         self._set_hidden(value)
577
578     @property
579     def x(self) -> int:
580         """X position of the left edge in the parent."""
581         return self._x
582
583     @x.setter
584     def x(self, value: int):
585         if not isinstance(value, int):
586             raise TypeError("X should be a integer type")
587         if self._x != value:
588             self._moved = True
589             self._x = value
590             if self._absolute_transform is not None:
591                 self._update_current_x()
592
593     @property
594     def y(self) -> int:
595         """Y position of the top edge in the parent."""
596         return self._y
597
598     @y.setter
599     def y(self, value: int):
600         if not isinstance(value, int):
601             raise TypeError("Y should be a integer type")
602         if self._y != value:
603             self._moved = True
604             self._y = value
605             if self._absolute_transform is not None:
606                 self._update_current_y()
607
608     @property
609     def flip_x(self) -> bool:
610         """If true, the left edge rendered will be the right edge of the right-most tile."""
611         return self._flip_x
612
613     @flip_x.setter
614     def flip_x(self, value: bool):
615         if not isinstance(value, bool):
616             raise TypeError("Flip X should be a boolean type")
617         if self._flip_x != value:
618             self._flip_x = value
619             self._full_change = True
620
621     @property
622     def flip_y(self) -> bool:
623         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
624         return self._flip_y
625
626     @flip_y.setter
627     def flip_y(self, value: bool):
628         if not isinstance(value, bool):
629             raise TypeError("Flip Y should be a boolean type")
630         if self._flip_y != value:
631             self._flip_y = value
632             self._full_change = True
633
634     @property
635     def transpose_xy(self) -> bool:
636         """If true, the TileGrid's axis will be swapped. When combined with mirroring, any 90
637         degree rotation can be achieved along with the corresponding mirrored version.
638         """
639         return self._transpose_xy
640
641     @transpose_xy.setter
642     def transpose_xy(self, value: bool) -> None:
643         if not isinstance(value, bool):
644             raise TypeError("Transpose XY should be a boolean type")
645         if self._transpose_xy != value:
646             self._transpose_xy = value
647             if self._pixel_width == self._pixel_height:
648                 self._full_change = True
649                 return
650             self._update_current_x()
651             self._update_current_y()
652             self._moved = True
653
654     @property
655     def pixel_shader(self) -> Union[ColorConverter, Palette]:
656         """The pixel shader of the tilegrid."""
657         return self._pixel_shader
658
659     @pixel_shader.setter
660     def pixel_shader(self, new_pixel_shader: Union[ColorConverter, Palette]) -> None:
661         if not isinstance(new_pixel_shader, ColorConverter) and not isinstance(
662             new_pixel_shader, Palette
663         ):
664             raise TypeError(
665                 "Unsupported Type: new_pixel_shader must be ColorConverter or Palette"
666             )
667
668         self._pixel_shader = new_pixel_shader
669         self._full_change = True
670
671     @property
672     def bitmap(self) -> Union[Bitmap, OnDiskBitmap, Shape]:
673         """The Bitmap, OnDiskBitmap, or Shape that is assigned to this TileGrid"""
674         return self._bitmap
675
676     @bitmap.setter
677     def bitmap(self, new_bitmap: Union[Bitmap, OnDiskBitmap, Shape]) -> None:
678         if (
679             not isinstance(new_bitmap, Bitmap)
680             and not isinstance(new_bitmap, OnDiskBitmap)
681             and not isinstance(new_bitmap, Shape)
682         ):
683             raise TypeError(
684                 "Unsupported Type: new_bitmap must be Bitmap, OnDiskBitmap, or Shape"
685             )
686
687         if (
688             new_bitmap.width != self.bitmap.width
689             or new_bitmap.height != self.bitmap.height
690         ):
691             raise ValueError("New bitmap must be same size as old bitmap")
692
693         self._bitmap = new_bitmap
694         self._full_change = True
695
696     def _extract_and_check_index(self, index):
697         if isinstance(index, (tuple, list)):
698             x = index[0]
699             y = index[1]
700             index = y * self._width_in_tiles + x
701         elif isinstance(index, int):
702             x = index % self._width_in_tiles
703             y = index // self._width_in_tiles
704         if (
705             x > self._width_in_tiles
706             or y > self._height_in_tiles
707             or index >= len(self._tiles)
708         ):
709             raise ValueError("Tile index out of bounds")
710         return x, y
711
712     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
713         """Returns the tile index at the given index. The index can either be
714         an x,y tuple or an int equal to ``y * width + x``'.
715         """
716         x, y = self._extract_and_check_index(index)
717         return self._tiles[y * self._width_in_tiles + x]
718
719     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
720         """Sets the tile index at the given index. The index can either be
721         an x,y tuple or an int equal to ``y * width + x``.
722         """
723         x, y = self._extract_and_check_index(index)
724         if not 0 <= value <= 255:
725             raise ValueError("Tile value out of bounds")
726         self._set_tile(x, y, value)
727
728     @property
729     def width(self) -> int:
730         """Width in tiles"""
731         return self._width_in_tiles
732
733     @property
734     def height(self) -> int:
735         """Height in tiles"""
736         return self._height_in_tiles
737
738     @property
739     def tile_width(self) -> int:
740         """Width of each tile in pixels"""
741         return self._tile_width
742
743     @property
744     def tile_height(self) -> int:
745         """Height of each tile in pixels"""
746         return self._tile_height