]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_tilegrid.py
bug fixes
[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 // 8] & (1 << (offset % 8)):
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[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[offset // pixels_per_byte] |= output_pixel.pixel << shift
402
403         return full_coverage
404
405     def _finish_refresh(self):
406         first_draw = self._previous_area.x1 == self._previous_area.x2
407         hidden = self._hidden_tilegrid or self._hidden_by_parent
408         if not first_draw and hidden:
409             self._previous_area.x2 = self._previous_area.x1
410         elif self._moved or first_draw:
411             self._current_area.copy_into(self._previous_area)
412
413         self._moved = False
414         self._full_change = False
415         self._partial_change = False
416         if isinstance(self._pixel_shader, (Palette, ColorConverter)):
417             self._pixel_shader._finish_refresh()  # pylint: disable=protected-access
418         if isinstance(self._bitmap, (Bitmap, Shape)):
419             self._bitmap._finish_refresh()  # pylint: disable=protected-access
420
421     def _get_refresh_areas(self, areas: list[Area]) -> None:
422         # pylint: disable=invalid-name, too-many-branches, too-many-statements
423         first_draw = self._previous_area.x1 == self._previous_area.x2
424         hidden = self._hidden_tilegrid or self._hidden_by_parent
425
426         # Check hidden first because it trumps all other changes
427         if hidden:
428             self._rendered_hidden = True
429             if not first_draw:
430                 areas.append(self._previous_area)
431             return
432         if self._moved and not first_draw:
433             self._previous_area.union(self._current_area, self._dirty_area)
434             if self._dirty_area.size() < 2 * self._pixel_width * self._pixel_height:
435                 areas.append(self._dirty_area)
436                 return
437             areas.append(self._current_area)
438             areas.append(self._previous_area)
439             return
440
441         tail = areas[-1] if areas else None
442         # If we have an in-memory bitmap, then check it for modifications
443         if isinstance(self._bitmap, Bitmap):
444             self._bitmap._get_refresh_areas(areas)  # pylint: disable=protected-access
445             refresh_area = areas[-1] if areas else None
446             if refresh_area != tail:
447                 # Special case a TileGrid that shows a full bitmap and use its
448                 # dirty area. Copy it to ours so we can transform it.
449                 if self._tiles_in_bitmap == 1:
450                     refresh_area.copy_into(self._dirty_area)
451                     self._partial_change = True
452                 else:
453                     self._full_change = True
454         elif isinstance(self._bitmap, Shape):
455             self._bitmap._get_refresh_areas(areas)  # pylint: disable=protected-access
456             refresh_area = areas[-1] if areas else None
457             if refresh_area != tail:
458                 refresh_area.copy_into(self._dirty_area)
459                 self._partial_change = True
460
461         self._full_change = self._full_change or (
462             isinstance(self._pixel_shader, (Palette, ColorConverter))
463             and self._pixel_shader._needs_refresh  # pylint: disable=protected-access
464         )
465         if self._full_change or first_draw:
466             areas.append(self._current_area)
467             return
468
469         if self._partial_change:
470             x = self._x
471             y = self._y
472             if self._absolute_transform.transpose_xy:
473                 x, y = y, x
474             x1 = self._dirty_area.x1
475             x2 = self._dirty_area.x2
476             if self._flip_x:
477                 x1 = self._pixel_width - x1
478                 x2 = self._pixel_width - x2
479             y1 = self._dirty_area.y1
480             y2 = self._dirty_area.y2
481             if self._flip_y:
482                 y1 = self._pixel_height - y1
483                 y2 = self._pixel_height - y2
484             if self._transpose_xy != self._absolute_transform.transpose_xy:
485                 x1, y1 = y1, x1
486                 x2, y2 = y2, x2
487             self._dirty_area.x1 = (
488                 self._absolute_transform.x + self._absolute_transform.dx * (x + x1)
489             )
490             self._dirty_area.y1 = (
491                 self._absolute_transform.y + self._absolute_transform.dy * (y + y1)
492             )
493             self._dirty_area.x2 = (
494                 self._absolute_transform.x + self._absolute_transform.dx * (x + x2)
495             )
496             self._dirty_area.y2 = (
497                 self._absolute_transform.y + self._absolute_transform.dy * (y + y2)
498             )
499             if self._dirty_area.y2 < self._dirty_area.y1:
500                 self._dirty_area.y1, self._dirty_area.y2 = (
501                     self._dirty_area.y2,
502                     self._dirty_area.y1,
503                 )
504             if self._dirty_area.x2 < self._dirty_area.x1:
505                 self._dirty_area.x1, self._dirty_area.x2 = (
506                     self._dirty_area.x2,
507                     self._dirty_area.x1,
508                 )
509             areas.append(self._dirty_area)
510
511     def _set_hidden(self, hidden: bool) -> None:
512         self._hidden_tilegrid = hidden
513         self._rendered_hidden = False
514         if not hidden:
515             self._full_change = True
516
517     def _set_hidden_by_parent(self, hidden: bool) -> None:
518         self._hidden_by_parent = hidden
519         self._rendered_hidden = False
520         if not hidden:
521             self._full_change = True
522
523     def _get_rendered_hidden(self) -> bool:
524         return self._rendered_hidden
525
526     def _set_all_tiles(self, tile_index: int) -> None:
527         """Set all tiles to the given tile index"""
528         if tile_index >= self._tiles_in_bitmap:
529             raise ValueError("Tile index out of bounds")
530         self._tiles = bytearray(
531             (self._width_in_tiles * self._height_in_tiles) * [tile_index]
532         )
533         self._full_change = True
534
535     def _set_tile(self, x: int, y: int, tile_index: int) -> None:
536         self._tiles[y * self._width_in_tiles + x] = tile_index
537         temp_area = Area()
538         if not self._partial_change:
539             tile_area = self._dirty_area
540         else:
541             tile_area = temp_area
542         top_x = (x - self._top_left_x) % self._width_in_tiles
543         if top_x < 0:
544             top_x += self._width_in_tiles
545         tile_area.x1 = top_x * self._tile_width
546         tile_area.x2 = tile_area.x1 + self._tile_width
547         top_y = (y - self._top_left_y) % self._height_in_tiles
548         if top_y < 0:
549             top_y += self._height_in_tiles
550         tile_area.y1 = top_y * self._tile_height
551         tile_area.y2 = tile_area.y1 + self._tile_height
552
553         if self._partial_change:
554             self._dirty_area.union(temp_area, self._dirty_area)
555
556         self._partial_change = True
557
558     def _set_top_left(self, x: int, y: int) -> None:
559         self._top_left_x = x
560         self._top_left_y = y
561         self._full_change = True
562
563     @property
564     def hidden(self) -> bool:
565         """True when the TileGrid is hidden. This may be False even
566         when a part of a hidden Group."""
567         return self._hidden_tilegrid
568
569     @hidden.setter
570     def hidden(self, value: bool):
571         if not isinstance(value, (bool, int)):
572             raise ValueError("Expecting a boolean or integer value")
573         value = bool(value)
574         self._set_hidden(value)
575
576     @property
577     def x(self) -> int:
578         """X position of the left edge in the parent."""
579         return self._x
580
581     @x.setter
582     def x(self, value: int):
583         if not isinstance(value, int):
584             raise TypeError("X should be a integer type")
585         if self._x != value:
586             self._moved = True
587             self._x = value
588             if self._absolute_transform is not None:
589                 self._update_current_x()
590
591     @property
592     def y(self) -> int:
593         """Y position of the top edge in the parent."""
594         return self._y
595
596     @y.setter
597     def y(self, value: int):
598         if not isinstance(value, int):
599             raise TypeError("Y should be a integer type")
600         if self._y != value:
601             self._moved = True
602             self._y = value
603             if self._absolute_transform is not None:
604                 self._update_current_y()
605
606     @property
607     def flip_x(self) -> bool:
608         """If true, the left edge rendered will be the right edge of the right-most tile."""
609         return self._flip_x
610
611     @flip_x.setter
612     def flip_x(self, value: bool):
613         if not isinstance(value, bool):
614             raise TypeError("Flip X should be a boolean type")
615         if self._flip_x != value:
616             self._flip_x = value
617             self._full_change = True
618
619     @property
620     def flip_y(self) -> bool:
621         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
622         return self._flip_y
623
624     @flip_y.setter
625     def flip_y(self, value: bool):
626         if not isinstance(value, bool):
627             raise TypeError("Flip Y should be a boolean type")
628         if self._flip_y != value:
629             self._flip_y = value
630             self._full_change = True
631
632     @property
633     def transpose_xy(self) -> bool:
634         """If true, the TileGrid's axis will be swapped. When combined with mirroring, any 90
635         degree rotation can be achieved along with the corresponding mirrored version.
636         """
637         return self._transpose_xy
638
639     @transpose_xy.setter
640     def transpose_xy(self, value: bool) -> None:
641         if not isinstance(value, bool):
642             raise TypeError("Transpose XY should be a boolean type")
643         if self._transpose_xy != value:
644             self._transpose_xy = value
645             if self._pixel_width == self._pixel_height:
646                 self._full_change = True
647                 return
648             self._update_current_x()
649             self._update_current_y()
650             self._moved = True
651
652     @property
653     def pixel_shader(self) -> Union[ColorConverter, Palette]:
654         """The pixel shader of the tilegrid."""
655         return self._pixel_shader
656
657     @pixel_shader.setter
658     def pixel_shader(self, new_pixel_shader: Union[ColorConverter, Palette]) -> None:
659         if not isinstance(new_pixel_shader, ColorConverter) and not isinstance(
660             new_pixel_shader, Palette
661         ):
662             raise TypeError(
663                 "Unsupported Type: new_pixel_shader must be ColorConverter or Palette"
664             )
665
666         self._pixel_shader = new_pixel_shader
667         self._full_change = True
668
669     @property
670     def bitmap(self) -> Union[Bitmap, OnDiskBitmap, Shape]:
671         """The Bitmap, OnDiskBitmap, or Shape that is assigned to this TileGrid"""
672         return self._bitmap
673
674     @bitmap.setter
675     def bitmap(self, new_bitmap: Union[Bitmap, OnDiskBitmap, Shape]) -> None:
676         if (
677             not isinstance(new_bitmap, Bitmap)
678             and not isinstance(new_bitmap, OnDiskBitmap)
679             and not isinstance(new_bitmap, Shape)
680         ):
681             raise TypeError(
682                 "Unsupported Type: new_bitmap must be Bitmap, OnDiskBitmap, or Shape"
683             )
684
685         if (
686             new_bitmap.width != self.bitmap.width
687             or new_bitmap.height != self.bitmap.height
688         ):
689             raise ValueError("New bitmap must be same size as old bitmap")
690
691         self._bitmap = new_bitmap
692         self._full_change = True
693
694     def _extract_and_check_index(self, index):
695         if isinstance(index, (tuple, list)):
696             x = index[0]
697             y = index[1]
698             index = y * self._width_in_tiles + x
699         elif isinstance(index, int):
700             x = index % self._width_in_tiles
701             y = index // self._width_in_tiles
702         if (
703             x > self._width_in_tiles
704             or y > self._height_in_tiles
705             or index >= len(self._tiles)
706         ):
707             raise ValueError("Tile index out of bounds")
708         return x, y
709
710     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
711         """Returns the tile index at the given index. The index can either be
712         an x,y tuple or an int equal to ``y * width + x``'.
713         """
714         x, y = self._extract_and_check_index(index)
715         return self._tiles[y * self._width_in_tiles + x]
716
717     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
718         """Sets the tile index at the given index. The index can either be
719         an x,y tuple or an int equal to ``y * width + x``.
720         """
721         x, y = self._extract_and_check_index(index)
722         if not 0 <= value <= 255:
723             raise ValueError("Tile value out of bounds")
724         self._set_tile(x, y, value)
725
726     @property
727     def width(self) -> int:
728         """Width in tiles"""
729         return self._width_in_tiles
730
731     @property
732     def height(self) -> int:
733         """Height in tiles"""
734         return self._height_in_tiles
735
736     @property
737     def tile_width(self) -> int:
738         """Width of each tile in pixels"""
739         return self._tile_width
740
741     @property
742     def tile_height(self) -> int:
743         """Height of each tile in pixels"""
744         return self._tile_height