]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/group.py
Merge pull request #1 from makermelissa/master
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / group.py
1 # The MIT License (MIT)
2 #
3 # Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining a copy
6 # of this software and associated documentation files (the "Software"), to deal
7 # in the Software without restriction, including without limitation the rights
8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 # copies of the Software, and to permit persons to whom the Software is
10 # furnished to do so, subject to the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 # THE SOFTWARE.
22
23 """
24 `displayio.group`
25 ================================================================================
26
27 displayio for Blinka
28
29 **Software and Dependencies:**
30
31 * Adafruit Blinka:
32   https://github.com/adafruit/Adafruit_Blinka/releases
33
34 * Author(s): Melissa LeBlanc-Williams
35
36 """
37
38 from recordclass import recordclass
39 from displayio.tilegrid import TileGrid
40
41 __version__ = "0.0.0-auto.0"
42 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
43
44
45 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
46
47
48 class Group:
49     """Manage a group of sprites and groups and how they are inter-related."""
50
51     def __init__(self, *, max_size=4, scale=1, x=0, y=0):
52         """Create a Group of a given size and scale. Scale is in
53         one dimension. For example, scale=2 leads to a layer’s
54         pixel being 2x2 pixels when in the group.
55         """
56         if not isinstance(max_size, int) or max_size < 1:
57             raise ValueError("Max Size must be >= 1")
58         self._max_size = max_size
59         if not isinstance(scale, int) or scale < 1:
60             raise ValueError("Scale must be >= 1")
61         self._scale = scale
62         self._x = x
63         self._y = y
64         self._hidden = False
65         self._layers = []
66         self._supported_types = (TileGrid, Group)
67         self._absolute_transform = None
68         self.in_group = False
69         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
70
71     def update_transform(self, parent_transform):
72         """Update the parent transform and child transforms"""
73         self.in_group = parent_transform is not None
74         if self.in_group:
75             x = self._x
76             y = self._y
77             if parent_transform.transpose_xy:
78                 x, y = y, x
79             self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
80             self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
81             self._absolute_transform.dx = parent_transform.dx * self._scale
82             self._absolute_transform.dy = parent_transform.dy * self._scale
83             self._absolute_transform.transpose_xy = parent_transform.transpose_xy
84             self._absolute_transform.mirror_x = parent_transform.mirror_x
85             self._absolute_transform.mirror_y = parent_transform.mirror_y
86             self._absolute_transform.scale = parent_transform.scale * self._scale
87         self._update_child_transforms()
88
89     def _update_child_transforms(self):
90         if self.in_group:
91             for layer in self._layers:
92                 layer.update_transform(self._absolute_transform)
93
94     def _removal_cleanup(self, index):
95         layer = self._layers[index]
96         layer.update_transform(None)
97
98     def _layer_update(self, index):
99         layer = self._layers[index]
100         layer.update_transform(self._absolute_transform)
101
102     def append(self, layer):
103         """Append a layer to the group. It will be drawn
104         above other layers.
105         """
106         self.insert(len(self._layers), layer)
107
108     def insert(self, index, layer):
109         """Insert a layer into the group."""
110         if not isinstance(layer, self._supported_types):
111             raise ValueError("Invalid Group Member")
112         if layer.in_group:
113             raise ValueError("Layer already in a group.")
114         if len(self._layers) == self._max_size:
115             raise RuntimeError("Group full")
116         self._layers.insert(index, layer)
117         self._layer_update(index)
118
119     def index(self, layer):
120         """Returns the index of the first copy of layer.
121         Raises ValueError if not found.
122         """
123         return self._layers.index(layer)
124
125     def pop(self, index=-1):
126         """Remove the ith item and return it."""
127         self._removal_cleanup(index)
128         return self._layers.pop(index)
129
130     def remove(self, layer):
131         """Remove the first copy of layer. Raises ValueError
132         if it is not present."""
133         index = self.index(layer)
134         self._layers.pop(index)
135
136     def __len__(self):
137         """Returns the number of layers in a Group"""
138         return len(self._layers)
139
140     def __getitem__(self, index):
141         """Returns the value at the given index."""
142         return self._layers[index]
143
144     def __setitem__(self, index, value):
145         """Sets the value at the given index."""
146         self._removal_cleanup(index)
147         self._layers[index] = value
148         self._layer_update(index)
149
150     def __delitem__(self, index):
151         """Deletes the value at the given index."""
152         del self._layers[index]
153
154     def _fill_area(self, buffer):
155         if self._hidden:
156             return
157
158         for layer in self._layers:
159             if isinstance(layer, (Group, TileGrid)):
160                 layer._fill_area(buffer)  # pylint: disable=protected-access
161
162     @property
163     def hidden(self):
164         """True when the Group and all of it’s layers are not visible. When False, the
165         Group’s layers are visible if they haven’t been hidden.
166         """
167         return self._hidden
168
169     @hidden.setter
170     def hidden(self, value):
171         if not isinstance(value, (bool, int)):
172             raise ValueError("Expecting a boolean or integer value")
173         self._hidden = bool(value)
174
175     @property
176     def scale(self):
177         """Scales each pixel within the Group in both directions. For example, when
178         scale=2 each pixel will be represented by 2x2 pixels.
179         """
180         return self._scale
181
182     @scale.setter
183     def scale(self, value):
184         if not isinstance(value, int) or value < 1:
185             raise ValueError("Scale must be >= 1")
186         if self._scale != value:
187             parent_scale = self._absolute_transform.scale / self._scale
188             self._absolute_transform.dx = (
189                 self._absolute_transform.dx / self._scale * value
190             )
191             self._absolute_transform.dy = (
192                 self._absolute_transform.dy / self._scale * value
193             )
194             self._absolute_transform.scale = parent_scale * value
195
196             self._scale = value
197             self._update_child_transforms()
198
199     @property
200     def x(self):
201         """X position of the Group in the parent."""
202         return self._x
203
204     @x.setter
205     def x(self, value):
206         if not isinstance(value, int):
207             raise ValueError("x must be an integer")
208         if self._x != value:
209             if self._absolute_transform.transpose_xy:
210                 dy_value = self._absolute_transform.dy / self._scale
211                 self._absolute_transform.y += dy_value * (value - self._x)
212             else:
213                 dx_value = self._absolute_transform.dx / self._scale
214                 self._absolute_transform.x += dx_value * (value - self._x)
215             self._x = value
216             self._update_child_transforms()
217
218     @property
219     def y(self):
220         """Y position of the Group in the parent."""
221         return self._y
222
223     @y.setter
224     def y(self, value):
225         if not isinstance(value, int):
226             raise ValueError("y must be an integer")
227         if self._y != value:
228             if self._absolute_transform.transpose_xy:
229                 dx_value = self._absolute_transform.dx / self._scale
230                 self._absolute_transform.x += dx_value * (value - self._y)
231             else:
232                 dy_value = self._absolute_transform.dy / self._scale
233                 self._absolute_transform.y += dy_value * (value - self._y)
234             self._y = value
235             self._update_child_transforms()