]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/group.py
3d8324f75293a11e471608b2f8141838f313bf21
[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._group_x = x
63         self._group_y = y
64         self._hidden_group = 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(
70             0, 0, 1, 1, self._scale, False, False, False
71         )
72
73     def update_transform(self, parent_transform):
74         """Update the parent transform and child transforms"""
75         self.in_group = parent_transform is not None
76         if self.in_group:
77             x = self._group_x
78             y = self._group_y
79             if parent_transform.transpose_xy:
80                 x, y = y, x
81             self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
82             self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
83             self._absolute_transform.dx = parent_transform.dx * self._scale
84             self._absolute_transform.dy = parent_transform.dy * self._scale
85             self._absolute_transform.transpose_xy = parent_transform.transpose_xy
86             self._absolute_transform.mirror_x = parent_transform.mirror_x
87             self._absolute_transform.mirror_y = parent_transform.mirror_y
88             self._absolute_transform.scale = parent_transform.scale * self._scale
89         self._update_child_transforms()
90
91     def _update_child_transforms(self):
92         if self.in_group:
93             for layer in self._layers:
94                 layer.update_transform(self._absolute_transform)
95
96     def _removal_cleanup(self, index):
97         layer = self._layers[index]
98         layer.update_transform(None)
99
100     def _layer_update(self, index):
101         layer = self._layers[index]
102         layer.update_transform(self._absolute_transform)
103
104     def append(self, layer):
105         """Append a layer to the group. It will be drawn
106         above other layers.
107         """
108         self.insert(len(self._layers), layer)
109
110     def insert(self, index, layer):
111         """Insert a layer into the group."""
112         if not isinstance(layer, self._supported_types):
113             raise ValueError("Invalid Group Member")
114         if layer.in_group:
115             raise ValueError("Layer already in a group.")
116         if len(self._layers) == self._max_size:
117             raise RuntimeError("Group full")
118         self._layers.insert(index, layer)
119         self._layer_update(index)
120
121     def index(self, layer):
122         """Returns the index of the first copy of layer.
123         Raises ValueError if not found.
124         """
125         return self._layers.index(layer)
126
127     def pop(self, index=-1):
128         """Remove the ith item and return it."""
129         self._removal_cleanup(index)
130         return self._layers.pop(index)
131
132     def remove(self, layer):
133         """Remove the first copy of layer. Raises ValueError
134         if it is not present."""
135         index = self.index(layer)
136         self._layers.pop(index)
137
138     def __len__(self):
139         """Returns the number of layers in a Group"""
140         return len(self._layers)
141
142     def __getitem__(self, index):
143         """Returns the value at the given index."""
144         return self._layers[index]
145
146     def __setitem__(self, index, value):
147         """Sets the value at the given index."""
148         self._removal_cleanup(index)
149         self._layers[index] = value
150         self._layer_update(index)
151
152     def __delitem__(self, index):
153         """Deletes the value at the given index."""
154         del self._layers[index]
155
156     def _fill_area(self, buffer):
157         if self._hidden_group:
158             return
159
160         for layer in self._layers:
161             if isinstance(layer, (Group, TileGrid)):
162                 layer._fill_area(buffer)  # pylint: disable=protected-access
163
164     @property
165     def hidden(self):
166         """True when the Group and all of it’s layers are not visible. When False, the
167         Group’s layers are visible if they haven’t been hidden.
168         """
169         return self._hidden_group
170
171     @hidden.setter
172     def hidden(self, value):
173         if not isinstance(value, (bool, int)):
174             raise ValueError("Expecting a boolean or integer value")
175         self._hidden_group = bool(value)
176
177     @property
178     def scale(self):
179         """Scales each pixel within the Group in both directions. For example, when
180         scale=2 each pixel will be represented by 2x2 pixels.
181         """
182         return self._scale
183
184     @scale.setter
185     def scale(self, value):
186         if not isinstance(value, int) or value < 1:
187             raise ValueError("Scale must be >= 1")
188         if self._scale != value:
189             parent_scale = self._absolute_transform.scale / self._scale
190             self._absolute_transform.dx = (
191                 self._absolute_transform.dx / self._scale * value
192             )
193             self._absolute_transform.dy = (
194                 self._absolute_transform.dy / self._scale * value
195             )
196             self._absolute_transform.scale = parent_scale * value
197
198             self._scale = value
199             self._update_child_transforms()
200
201     @property
202     def x(self):
203         """X position of the Group in the parent."""
204         return self._group_x
205
206     @x.setter
207     def x(self, value):
208         if not isinstance(value, int):
209             raise ValueError("x must be an integer")
210         if self._group_x != value:
211             if self._absolute_transform.transpose_xy:
212                 dy_value = self._absolute_transform.dy / self._scale
213                 self._absolute_transform.y += dy_value * (value - self._group_x)
214             else:
215                 dx_value = self._absolute_transform.dx / self._scale
216                 self._absolute_transform.x += dx_value * (value - self._group_x)
217             self._group_x = value
218             self._update_child_transforms()
219
220     @property
221     def y(self):
222         """Y position of the Group in the parent."""
223         return self._group_y
224
225     @y.setter
226     def y(self, value):
227         if not isinstance(value, int):
228             raise ValueError("y must be an integer")
229         if self._group_y != value:
230             if self._absolute_transform.transpose_xy:
231                 dx_value = self._absolute_transform.dx / self._scale
232                 self._absolute_transform.x += dx_value * (value - self._group_y)
233             else:
234                 dy_value = self._absolute_transform.dy / self._scale
235                 self._absolute_transform.y += dy_value * (value - self._group_y)
236             self._group_y = value
237             self._update_child_transforms()