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