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(
70 0, 0, 1, 1, self._scale, False, False, False
73 def update_transform(self, parent_transform):
74 """Update the parent transform and child transforms"""
75 self.in_group = parent_transform is not None
79 if parent_transform.transpose_xy:
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()
91 def _update_child_transforms(self):
93 for layer in self._layers:
94 layer.update_transform(self._absolute_transform)
96 def _removal_cleanup(self, index):
97 layer = self._layers[index]
98 layer.update_transform(None)
100 def _layer_update(self, index):
101 layer = self._layers[index]
102 layer.update_transform(self._absolute_transform)
104 def append(self, layer):
105 """Append a layer to the group. It will be drawn
108 self.insert(len(self._layers), layer)
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")
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)
121 def index(self, layer):
122 """Returns the index of the first copy of layer.
123 Raises ValueError if not found.
125 return self._layers.index(layer)
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)
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)
139 """Returns the number of layers in a Group"""
140 return len(self._layers)
142 def __getitem__(self, index):
143 """Returns the value at the given index."""
144 return self._layers[index]
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)
152 def __delitem__(self, index):
153 """Deletes the value at the given index."""
154 del self._layers[index]
156 def _fill_area(self, buffer):
160 for layer in self._layers:
161 if isinstance(layer, (Group, TileGrid)):
162 layer._fill_area(buffer) # pylint: disable=protected-access
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.
172 def hidden(self, value):
173 if not isinstance(value, (bool, int)):
174 raise ValueError("Expecting a boolean or integer value")
175 self._hidden = bool(value)
179 """Scales each pixel within the Group in both directions. For example, when
180 scale=2 each pixel will be represented by 2x2 pixels.
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
193 self._absolute_transform.dy = (
194 self._absolute_transform.dy / self._scale * value
196 self._absolute_transform.scale = parent_scale * value
199 self._update_child_transforms()
203 """X position of the Group in the parent."""
208 if not isinstance(value, int):
209 raise ValueError("x must be an integer")
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._x)
215 dx_value = self._absolute_transform.dx / self._scale
216 self._absolute_transform.x += dx_value * (value - self._x)
218 self._update_child_transforms()
222 """Y position of the Group in the parent."""
227 if not isinstance(value, int):
228 raise ValueError("y must be an integer")
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._y)
234 dy_value = self._absolute_transform.dy / self._scale
235 self._absolute_transform.y += dy_value * (value - self._y)
237 self._update_child_transforms()