1 # The MIT License (MIT)
3 # Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
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:
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
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
25 ================================================================================
29 **Software and Dependencies:**
32 https://github.com/adafruit/Adafruit_Blinka/releases
34 * Author(s): Melissa LeBlanc-Williams
38 from recordclass import recordclass
39 from displayio.tilegrid import TileGrid
41 __version__ = "0.0.0-auto.0"
42 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
45 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
49 """Manage a group of sprites and groups and how they are inter-related."""
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.
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")
66 self._supported_types = (TileGrid, Group)
67 self._absolute_transform = None
69 self._absolute_transform = Transform(0, 0, 1, 1, self._scale, False, False, False)
71 def update_transform(self, parent_transform):
72 """Update the parent transform and child transforms"""
73 self.in_group = parent_transform is not None
77 if parent_transform.transpose_xy:
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()
89 def _update_child_transforms(self):
91 for layer in self._layers:
92 layer.update_transform(self._absolute_transform)
94 def _removal_cleanup(self, index):
95 layer = self._layers[index]
96 layer.update_transform(None)
98 def _layer_update(self, index):
99 layer = self._layers[index]
100 layer.update_transform(self._absolute_transform)
102 def append(self, layer):
103 """Append a layer to the group. It will be drawn
106 self.insert(len(self._layers), layer)
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")
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)
119 def index(self, layer):
120 """Returns the index of the first copy of layer.
121 Raises ValueError if not found.
123 return self._layers.index(layer)
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)
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)
137 """Returns the number of layers in a Group"""
138 return len(self._layers)
140 def __getitem__(self, index):
141 """Returns the value at the given index."""
142 return self._layers[index]
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)
150 def __delitem__(self, index):
151 """Deletes the value at the given index."""
152 del self._layers[index]
154 def _fill_area(self, buffer):
158 for layer in self._layers:
159 if isinstance(layer, (Group, TileGrid)):
160 layer._fill_area(buffer) # pylint: disable=protected-access
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.
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)
177 """Scales each pixel within the Group in both directions. For example, when
178 scale=2 each pixel will be represented by 2x2 pixels.
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
191 self._absolute_transform.dy = (
192 self._absolute_transform.dy / self._scale * value
194 self._absolute_transform.scale = parent_scale * value
197 self._update_child_transforms()
201 """X position of the Group in the parent."""
206 if not isinstance(value, int):
207 raise ValueError("x must be an integer")
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)
213 dx_value = self._absolute_transform.dx / self._scale
214 self._absolute_transform.x += dx_value * (value - self._x)
216 self._update_child_transforms()
220 """Y position of the Group in the parent."""
225 if not isinstance(value, int):
226 raise ValueError("y must be an integer")
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)
232 dy_value = self._absolute_transform.dy / self._scale
233 self._absolute_transform.y += dy_value * (value - self._y)
235 self._update_child_transforms()