amulet-core 1.9.19__py3-none-any.whl → 1.9.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of amulet-core might be problematic. Click here for more details.

Files changed (198) hide show
  1. amulet/__init__.py +27 -27
  2. amulet/__pyinstaller/__init__.py +2 -2
  3. amulet/__pyinstaller/hook-amulet.py +4 -4
  4. amulet/_version.py +21 -21
  5. amulet/api/__init__.py +2 -2
  6. amulet/api/abstract_base_entity.py +128 -128
  7. amulet/api/block.py +630 -630
  8. amulet/api/block_entity.py +71 -71
  9. amulet/api/cache.py +107 -107
  10. amulet/api/chunk/__init__.py +6 -6
  11. amulet/api/chunk/biomes.py +207 -207
  12. amulet/api/chunk/block_entity_dict.py +175 -175
  13. amulet/api/chunk/blocks.py +46 -46
  14. amulet/api/chunk/chunk.py +389 -389
  15. amulet/api/chunk/entity_list.py +75 -75
  16. amulet/api/chunk/status.py +167 -167
  17. amulet/api/data_types/__init__.py +4 -4
  18. amulet/api/data_types/generic_types.py +4 -4
  19. amulet/api/data_types/operation_types.py +16 -16
  20. amulet/api/data_types/world_types.py +49 -49
  21. amulet/api/data_types/wrapper_types.py +71 -71
  22. amulet/api/entity.py +74 -74
  23. amulet/api/errors.py +119 -119
  24. amulet/api/history/__init__.py +36 -36
  25. amulet/api/history/base/__init__.py +3 -3
  26. amulet/api/history/base/base_history.py +26 -26
  27. amulet/api/history/base/history_manager.py +63 -63
  28. amulet/api/history/base/revision_manager.py +73 -73
  29. amulet/api/history/changeable.py +15 -15
  30. amulet/api/history/data_types.py +7 -7
  31. amulet/api/history/history_manager/__init__.py +3 -3
  32. amulet/api/history/history_manager/container.py +102 -102
  33. amulet/api/history/history_manager/database.py +279 -279
  34. amulet/api/history/history_manager/meta.py +93 -93
  35. amulet/api/history/history_manager/object.py +116 -116
  36. amulet/api/history/revision_manager/__init__.py +2 -2
  37. amulet/api/history/revision_manager/disk.py +33 -33
  38. amulet/api/history/revision_manager/ram.py +12 -12
  39. amulet/api/item.py +75 -75
  40. amulet/api/level/__init__.py +4 -4
  41. amulet/api/level/base_level/__init__.py +1 -1
  42. amulet/api/level/base_level/base_level.py +1035 -1026
  43. amulet/api/level/base_level/chunk_manager.py +227 -227
  44. amulet/api/level/base_level/clone.py +389 -389
  45. amulet/api/level/base_level/player_manager.py +101 -101
  46. amulet/api/level/immutable_structure/__init__.py +1 -1
  47. amulet/api/level/immutable_structure/immutable_structure.py +94 -94
  48. amulet/api/level/immutable_structure/void_format_wrapper.py +117 -117
  49. amulet/api/level/structure.py +22 -22
  50. amulet/api/level/world.py +19 -19
  51. amulet/api/partial_3d_array/__init__.py +2 -2
  52. amulet/api/partial_3d_array/base_partial_3d_array.py +263 -263
  53. amulet/api/partial_3d_array/bounded_partial_3d_array.py +528 -528
  54. amulet/api/partial_3d_array/data_types.py +15 -15
  55. amulet/api/partial_3d_array/unbounded_partial_3d_array.py +229 -229
  56. amulet/api/partial_3d_array/util.py +152 -152
  57. amulet/api/player.py +65 -65
  58. amulet/api/registry/__init__.py +2 -2
  59. amulet/api/registry/base_registry.py +34 -34
  60. amulet/api/registry/biome_manager.py +153 -153
  61. amulet/api/registry/block_manager.py +156 -156
  62. amulet/api/selection/__init__.py +2 -2
  63. amulet/api/selection/abstract_selection.py +315 -315
  64. amulet/api/selection/box.py +805 -805
  65. amulet/api/selection/group.py +488 -488
  66. amulet/api/structure.py +37 -37
  67. amulet/api/wrapper/__init__.py +8 -8
  68. amulet/api/wrapper/chunk/interface.py +441 -441
  69. amulet/api/wrapper/chunk/translator.py +567 -567
  70. amulet/api/wrapper/format_wrapper.py +772 -772
  71. amulet/api/wrapper/structure_format_wrapper.py +116 -116
  72. amulet/api/wrapper/world_format_wrapper.py +63 -63
  73. amulet/level/__init__.py +1 -1
  74. amulet/level/formats/anvil_forge_world.py +40 -40
  75. amulet/level/formats/anvil_world/__init__.py +3 -3
  76. amulet/level/formats/anvil_world/_sector_manager.py +291 -384
  77. amulet/level/formats/anvil_world/data_pack/__init__.py +2 -2
  78. amulet/level/formats/anvil_world/data_pack/data_pack.py +224 -224
  79. amulet/level/formats/anvil_world/data_pack/data_pack_manager.py +77 -77
  80. amulet/level/formats/anvil_world/dimension.py +177 -177
  81. amulet/level/formats/anvil_world/format.py +769 -769
  82. amulet/level/formats/anvil_world/region.py +384 -384
  83. amulet/level/formats/construction/__init__.py +3 -3
  84. amulet/level/formats/construction/format_wrapper.py +515 -515
  85. amulet/level/formats/construction/interface.py +134 -134
  86. amulet/level/formats/construction/section.py +60 -60
  87. amulet/level/formats/construction/util.py +165 -165
  88. amulet/level/formats/leveldb_world/__init__.py +3 -3
  89. amulet/level/formats/leveldb_world/chunk.py +33 -33
  90. amulet/level/formats/leveldb_world/dimension.py +385 -419
  91. amulet/level/formats/leveldb_world/format.py +659 -641
  92. amulet/level/formats/leveldb_world/interface/chunk/__init__.py +36 -36
  93. amulet/level/formats/leveldb_world/interface/chunk/base_leveldb_interface.py +836 -836
  94. amulet/level/formats/leveldb_world/interface/chunk/generate_interface.py +31 -31
  95. amulet/level/formats/leveldb_world/interface/chunk/leveldb_0.py +30 -30
  96. amulet/level/formats/leveldb_world/interface/chunk/leveldb_1.py +12 -12
  97. amulet/level/formats/leveldb_world/interface/chunk/leveldb_10.py +12 -12
  98. amulet/level/formats/leveldb_world/interface/chunk/leveldb_11.py +12 -12
  99. amulet/level/formats/leveldb_world/interface/chunk/leveldb_12.py +12 -12
  100. amulet/level/formats/leveldb_world/interface/chunk/leveldb_13.py +12 -12
  101. amulet/level/formats/leveldb_world/interface/chunk/leveldb_14.py +12 -12
  102. amulet/level/formats/leveldb_world/interface/chunk/leveldb_15.py +12 -12
  103. amulet/level/formats/leveldb_world/interface/chunk/leveldb_16.py +12 -12
  104. amulet/level/formats/leveldb_world/interface/chunk/leveldb_17.py +12 -12
  105. amulet/level/formats/leveldb_world/interface/chunk/leveldb_18.py +12 -12
  106. amulet/level/formats/leveldb_world/interface/chunk/leveldb_19.py +12 -12
  107. amulet/level/formats/leveldb_world/interface/chunk/leveldb_2.py +12 -12
  108. amulet/level/formats/leveldb_world/interface/chunk/leveldb_20.py +12 -12
  109. amulet/level/formats/leveldb_world/interface/chunk/leveldb_21.py +12 -12
  110. amulet/level/formats/leveldb_world/interface/chunk/leveldb_22.py +12 -12
  111. amulet/level/formats/leveldb_world/interface/chunk/leveldb_23.py +10 -10
  112. amulet/level/formats/leveldb_world/interface/chunk/leveldb_24.py +10 -10
  113. amulet/level/formats/leveldb_world/interface/chunk/leveldb_25.py +24 -24
  114. amulet/level/formats/leveldb_world/interface/chunk/leveldb_26.py +10 -10
  115. amulet/level/formats/leveldb_world/interface/chunk/leveldb_27.py +10 -10
  116. amulet/level/formats/leveldb_world/interface/chunk/leveldb_28.py +10 -10
  117. amulet/level/formats/leveldb_world/interface/chunk/leveldb_29.py +33 -33
  118. amulet/level/formats/leveldb_world/interface/chunk/leveldb_3.py +57 -57
  119. amulet/level/formats/leveldb_world/interface/chunk/leveldb_30.py +10 -10
  120. amulet/level/formats/leveldb_world/interface/chunk/leveldb_31.py +10 -10
  121. amulet/level/formats/leveldb_world/interface/chunk/leveldb_32.py +10 -10
  122. amulet/level/formats/leveldb_world/interface/chunk/leveldb_33.py +10 -10
  123. amulet/level/formats/leveldb_world/interface/chunk/leveldb_34.py +10 -10
  124. amulet/level/formats/leveldb_world/interface/chunk/leveldb_35.py +10 -10
  125. amulet/level/formats/leveldb_world/interface/chunk/leveldb_36.py +10 -10
  126. amulet/level/formats/leveldb_world/interface/chunk/leveldb_37.py +10 -10
  127. amulet/level/formats/leveldb_world/interface/chunk/leveldb_38.py +10 -10
  128. amulet/level/formats/leveldb_world/interface/chunk/leveldb_39.py +12 -12
  129. amulet/level/formats/leveldb_world/interface/chunk/leveldb_4.py +12 -12
  130. amulet/level/formats/leveldb_world/interface/chunk/leveldb_40.py +16 -16
  131. amulet/level/formats/leveldb_world/interface/chunk/leveldb_5.py +12 -12
  132. amulet/level/formats/leveldb_world/interface/chunk/leveldb_6.py +12 -12
  133. amulet/level/formats/leveldb_world/interface/chunk/leveldb_7.py +12 -12
  134. amulet/level/formats/leveldb_world/interface/chunk/leveldb_8.py +180 -180
  135. amulet/level/formats/leveldb_world/interface/chunk/leveldb_9.py +18 -18
  136. amulet/level/formats/leveldb_world/interface/chunk/leveldb_chunk_versions.py +79 -79
  137. amulet/level/formats/mcstructure/__init__.py +3 -3
  138. amulet/level/formats/mcstructure/chunk.py +50 -50
  139. amulet/level/formats/mcstructure/format_wrapper.py +408 -408
  140. amulet/level/formats/mcstructure/interface.py +175 -175
  141. amulet/level/formats/schematic/__init__.py +3 -3
  142. amulet/level/formats/schematic/chunk.py +55 -55
  143. amulet/level/formats/schematic/data_types.py +4 -4
  144. amulet/level/formats/schematic/format_wrapper.py +373 -373
  145. amulet/level/formats/schematic/interface.py +142 -142
  146. amulet/level/formats/sponge_schem/__init__.py +4 -4
  147. amulet/level/formats/sponge_schem/chunk.py +62 -62
  148. amulet/level/formats/sponge_schem/format_wrapper.py +463 -463
  149. amulet/level/formats/sponge_schem/interface.py +118 -118
  150. amulet/level/formats/sponge_schem/varint/__init__.py +1 -1
  151. amulet/level/formats/sponge_schem/varint/varint.py +87 -87
  152. amulet/level/interfaces/chunk/anvil/anvil_0.py +72 -72
  153. amulet/level/interfaces/chunk/anvil/anvil_1444.py +336 -336
  154. amulet/level/interfaces/chunk/anvil/anvil_1466.py +94 -94
  155. amulet/level/interfaces/chunk/anvil/anvil_1467.py +37 -37
  156. amulet/level/interfaces/chunk/anvil/anvil_1484.py +20 -20
  157. amulet/level/interfaces/chunk/anvil/anvil_1503.py +20 -20
  158. amulet/level/interfaces/chunk/anvil/anvil_1519.py +34 -34
  159. amulet/level/interfaces/chunk/anvil/anvil_1901.py +20 -20
  160. amulet/level/interfaces/chunk/anvil/anvil_1908.py +20 -20
  161. amulet/level/interfaces/chunk/anvil/anvil_1912.py +21 -21
  162. amulet/level/interfaces/chunk/anvil/anvil_1934.py +20 -20
  163. amulet/level/interfaces/chunk/anvil/anvil_2203.py +69 -69
  164. amulet/level/interfaces/chunk/anvil/anvil_2529.py +19 -19
  165. amulet/level/interfaces/chunk/anvil/anvil_2681.py +76 -76
  166. amulet/level/interfaces/chunk/anvil/anvil_2709.py +19 -19
  167. amulet/level/interfaces/chunk/anvil/anvil_2844.py +267 -267
  168. amulet/level/interfaces/chunk/anvil/anvil_3463.py +19 -19
  169. amulet/level/interfaces/chunk/anvil/anvil_na.py +607 -607
  170. amulet/level/interfaces/chunk/anvil/base_anvil_interface.py +326 -326
  171. amulet/level/load.py +59 -59
  172. amulet/level/loader.py +95 -95
  173. amulet/level/translators/chunk/bedrock/__init__.py +267 -267
  174. amulet/level/translators/chunk/bedrock/bedrock_nbt_blockstate_translator.py +46 -46
  175. amulet/level/translators/chunk/bedrock/bedrock_numerical_translator.py +39 -39
  176. amulet/level/translators/chunk/bedrock/bedrock_psudo_numerical_translator.py +37 -37
  177. amulet/level/translators/chunk/java/java_1_18_translator.py +40 -40
  178. amulet/level/translators/chunk/java/java_blockstate_translator.py +94 -94
  179. amulet/level/translators/chunk/java/java_numerical_translator.py +62 -62
  180. amulet/libs/leveldb/__init__.py +7 -7
  181. amulet/operations/__init__.py +5 -5
  182. amulet/operations/clone.py +18 -18
  183. amulet/operations/delete_chunk.py +32 -32
  184. amulet/operations/fill.py +30 -30
  185. amulet/operations/paste.py +65 -65
  186. amulet/operations/replace.py +58 -58
  187. amulet/utils/__init__.py +14 -14
  188. amulet/utils/format_utils.py +41 -41
  189. amulet/utils/generator.py +15 -15
  190. amulet/utils/matrix.py +243 -243
  191. amulet/utils/numpy_helpers.py +46 -46
  192. amulet/utils/world_utils.py +349 -349
  193. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/METADATA +97 -97
  194. amulet_core-1.9.20.dist-info/RECORD +208 -0
  195. amulet_core-1.9.19.dist-info/RECORD +0 -208
  196. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/WHEEL +0 -0
  197. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/entry_points.txt +0 -0
  198. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/top_level.txt +0 -0
@@ -1,488 +1,488 @@
1
- from __future__ import annotations
2
-
3
- import itertools
4
- import numpy
5
-
6
- from typing import Tuple, Iterable, List, Union, Optional, overload, Set
7
-
8
- from amulet.api.data_types import (
9
- BlockCoordinates,
10
- CoordinatesAny,
11
- ChunkCoordinates,
12
- SubChunkCoordinates,
13
- FloatTriplet,
14
- PointCoordinatesAny,
15
- )
16
- from .abstract_selection import AbstractBaseSelection
17
- from .box import SelectionBox
18
-
19
-
20
- class SelectionGroup(AbstractBaseSelection):
21
- """
22
- A container for zero or more :class:`SelectionBox` instances.
23
-
24
- This allows for non-rectangular and non-contiguous selections.
25
- """
26
-
27
- def __init__(
28
- self, selection_boxes: Union[SelectionBox, Iterable[SelectionBox]] = ()
29
- ):
30
- """
31
- Construct a new :class:`SelectionGroup` class from the given data.
32
-
33
- >>> SelectionGroup(SelectionBox((0, 0, 0), (1, 1, 1)))
34
- >>> SelectionGroup([
35
- >>> SelectionBox((0, 0, 0), (1, 1, 1)),
36
- >>> SelectionBox((1, 1, 1), (2, 2, 2))
37
- >>> ])
38
-
39
- :param selection_boxes: A :class:`SelectionBox` or iterable of :class:`SelectionBox` classes.
40
- """
41
- if isinstance(selection_boxes, SelectionBox):
42
- self._selection_boxes: Tuple[SelectionBox, ...] = (selection_boxes,)
43
- else:
44
- self._selection_boxes: Tuple[SelectionBox, ...] = tuple(
45
- box for box in selection_boxes if isinstance(box, SelectionBox)
46
- )
47
-
48
- def __repr__(self) -> str:
49
- boxes = ", ".join([repr(box) for box in self.selection_boxes])
50
- return f"SelectionGroup([{boxes}])"
51
-
52
- def __str__(self) -> str:
53
- boxes = ", ".join([str(box) for box in self.selection_boxes])
54
- return f"[{boxes}]"
55
-
56
- def __eq__(self, other: SelectionGroup) -> bool:
57
- """
58
- Does the contents of this :class:`SelectionGroup` match the other :class:`SelectionGroup`.
59
-
60
- Note if the boxes do not exactly match this will return False even if the volume represented is the same.
61
-
62
- :param other: The other :class:`SelectionGroup` to compare with.
63
- :return: True if the boxes contained match.
64
- """
65
- return (
66
- isinstance(other, SelectionGroup)
67
- and self.selection_boxes_sorted == other.selection_boxes_sorted
68
- )
69
-
70
- def __add__(self, boxes: Iterable[SelectionBox]) -> SelectionGroup:
71
- """
72
- Add an iterable of :class:`SelectionBox` classes to this :class:`SelectionGroup`.
73
-
74
- Note this will construct a new :class:`SelectionGroup` because it is immutable so cannot be modified in place.
75
-
76
- >>> group1 = SelectionGroup(SelectionBox((-1, -1, -1), (0, 0, 0)))
77
- >>> group2 = SelectionGroup([
78
- >>> SelectionBox((0, 0, 0), (1, 1, 1)),
79
- >>> SelectionBox((1, 1, 1), (2, 2, 2))
80
- >>> ])
81
- >>> group1 + group2
82
- SelectionGroup([SelectionBox((-1, -1, -1), (0, 0, 0)), SelectionBox((0, 0, 0), (1, 1, 1)), SelectionBox((1, 1, 1), (2, 2, 2))])
83
- >>> group1 += group2
84
- >>> group1
85
- SelectionGroup([SelectionBox((-1, -1, -1), (0, 0, 0)), SelectionBox((0, 0, 0), (1, 1, 1)), SelectionBox((1, 1, 1), (2, 2, 2))])
86
-
87
- :param boxes: An iterable of boxes to add to this group.
88
- :return: A new :class:`SelectionGroup` class containing the boxes from this instance and those in ``boxes``.
89
- """
90
- try:
91
- boxes = tuple(boxes)
92
- except:
93
- raise NotImplemented
94
- if all(isinstance(b, SelectionBox) for b in boxes):
95
- return SelectionGroup(tuple(self) + boxes)
96
- raise NotImplemented
97
-
98
- def __iter__(self) -> Iterable[SelectionBox]:
99
- """An iterable of all the :class:`SelectionBox` classes in the group."""
100
- yield from self._selection_boxes
101
-
102
- def __len__(self) -> int:
103
- """The number of :class:`SelectionBox` classes in the group."""
104
- return len(self._selection_boxes)
105
-
106
- def __contains__(self, item: CoordinatesAny) -> bool:
107
- return self.contains_block(item)
108
-
109
- def contains_block(self, coords: CoordinatesAny) -> bool:
110
- return any(box.contains_block(coords) for box in self._selection_boxes)
111
-
112
- def contains_point(self, coords: CoordinatesAny) -> bool:
113
- return any(box.contains_point(coords) for box in self._selection_boxes)
114
-
115
- @property
116
- def blocks(self) -> Iterable[BlockCoordinates]:
117
- """
118
- The location of every block in the selection.
119
-
120
- >>> for x, y, z in group.blocks:
121
- >>> ...
122
-
123
- Note: if boxes intersect, the blocks in the intersected region will be included multiple times.
124
-
125
- If this behaviour is not desired the :meth:`merge_boxes` method will return a new SelectionGroup with no intersections.
126
-
127
- >>> for x, y, z in group.merge_boxes().blocks:
128
- >>> ...
129
-
130
- :return: An iterable of block locations.
131
- """
132
- return itertools.chain.from_iterable(self.selection_boxes)
133
-
134
- def __bool__(self) -> bool:
135
- """Are there any boxes in the group."""
136
- return bool(self._selection_boxes)
137
-
138
- @overload
139
- def __getitem__(self, item: int) -> SelectionBox:
140
- ...
141
-
142
- @overload
143
- def __getitem__(self, item: slice) -> SelectionGroup:
144
- ...
145
-
146
- def __getitem__(self, item):
147
- """Get the selection box at the given index."""
148
- val = self._selection_boxes[item]
149
- if isinstance(val, tuple):
150
- return SelectionGroup(val)
151
- else:
152
- return val
153
-
154
- @property
155
- def min(self) -> BlockCoordinates:
156
- """The minimum point of of all the boxes in the group."""
157
- return tuple(self.min_array.tolist())
158
-
159
- @property
160
- def min_array(self) -> numpy.ndarray:
161
- """The minimum point of of all the boxes in the group as a numpy array."""
162
- if self._selection_boxes:
163
- return numpy.min(numpy.array([box.min for box in self._selection_boxes]), 0)
164
- else:
165
- raise ValueError("SelectionGroup does not contain any SelectionBoxes")
166
-
167
- @property
168
- def min_x(self) -> int:
169
- return int(self.min_array[0])
170
-
171
- @property
172
- def min_y(self) -> int:
173
- return int(self.min_array[1])
174
-
175
- @property
176
- def min_z(self) -> int:
177
- return int(self.min_array[2])
178
-
179
- @property
180
- def max(self) -> BlockCoordinates:
181
- """The maximum point of of all the boxes in the group."""
182
- return tuple(self.max_array.tolist())
183
-
184
- @property
185
- def max_array(self) -> numpy.ndarray:
186
- """The maximum point of of all the boxes in the group as a numpy array."""
187
- if self._selection_boxes:
188
- return numpy.max(numpy.array([box.max for box in self._selection_boxes]), 0)
189
- else:
190
- raise ValueError("SelectionGroup does not contain any SelectionBoxes")
191
-
192
- @property
193
- def max_x(self) -> int:
194
- return int(self.max_array[0])
195
-
196
- @property
197
- def max_y(self) -> int:
198
- return int(self.max_array[1])
199
-
200
- @property
201
- def max_z(self) -> int:
202
- return int(self.max_array[2])
203
-
204
- @property
205
- def bounds(self) -> Tuple[BlockCoordinates, BlockCoordinates]:
206
- return self.min, self.max
207
-
208
- @property
209
- def bounds_array(self) -> numpy.ndarray:
210
- return numpy.array([self.min_array, self.max_array])
211
-
212
- def to_box(self) -> SelectionBox:
213
- """Create a `SelectionBox` based off the bounds of the boxes in the group."""
214
- return SelectionBox(self.min, self.max)
215
-
216
- def merge_boxes(self) -> SelectionGroup:
217
- """
218
- Take the boxes as they were given to this class, merge neighbouring boxes and remove overlapping regions.
219
-
220
- The result should be a SelectionGroup containing one or more SelectionBox classes that represents the same
221
- volume as the original but with no overlapping boxes.
222
- """
223
- selection_boxes = self.selection_boxes
224
- # TODO remove duplicate boxes
225
-
226
- # remove duplicates
227
- selection_boxes_ = []
228
- for box in selection_boxes:
229
- if not any(box == box_ for box_ in selection_boxes_):
230
- selection_boxes_.append(box)
231
- selection_boxes = selection_boxes_
232
-
233
- if len(selection_boxes) >= 2:
234
- merge_boxes = True
235
- while merge_boxes:
236
- # find two neighbouring boxes and merge them
237
- merge_boxes = False # if two boxes get merged this will be set back to True and this will run again.
238
- box_index = 0 # the index of the first box
239
- while box_index < len(selection_boxes):
240
- box = selection_boxes[box_index]
241
- other_index = box_index + 1 # the index of the second box.
242
- # This always starts at one greater than box_index because
243
- # the lower values were already checked the other way around
244
- while other_index < len(selection_boxes):
245
- other = selection_boxes[other_index]
246
- x_dim = box.min_x == other.min_x and box.max_x == other.max_x
247
- y_dim = box.min_y == other.min_y and box.max_y == other.max_y
248
- z_dim = box.min_z == other.min_z and box.max_z == other.max_z
249
-
250
- x_border = box.max_x == other.min_x or other.max_x == box.min_x
251
- y_border = box.max_y == other.min_y or other.max_y == box.min_y
252
- z_border = box.max_z == other.min_z or other.max_z == box.min_z
253
-
254
- if (
255
- (x_dim and y_dim and z_border)
256
- or (x_dim and z_dim and y_border)
257
- or (y_dim and z_dim and x_border)
258
- ):
259
- selection_boxes.pop(other_index)
260
- selection_boxes.pop(box_index)
261
- selection_boxes.append(
262
- SelectionBox(
263
- numpy.min([box.min, other.min], 0),
264
- numpy.max([box.max, other.max], 0),
265
- )
266
- )
267
- merge_boxes = True
268
- box = selection_boxes[box_index]
269
- other_index = box_index + 1
270
- else:
271
- other_index += 1
272
- box_index += 1
273
- return SelectionGroup(selection_boxes)
274
-
275
- @property
276
- def is_contiguous(self) -> bool:
277
- """
278
- Does the SelectionGroup represent one connected region (True) or multiple separated regions (False).
279
-
280
- If two boxes are touching at the corners this is classed as contiguous.
281
- """
282
- # TODO: This needs some work. It will only work if the selections touch in a chain
283
- # it does not care if it loops back and intersects itself. Does intersecting count as being contiguous?
284
- # I would say yes
285
- if len(self._selection_boxes) == 1:
286
- return True
287
-
288
- for i in range(len(self._selection_boxes) - 1):
289
- sub_box: SelectionBox = self._selection_boxes[i]
290
- next_box: SelectionBox = self._selection_boxes[i + 1]
291
- if not sub_box.touches(next_box):
292
- return False
293
-
294
- return True
295
-
296
- @property
297
- def is_rectangular(self) -> bool:
298
- """
299
- Checks if the SelectionGroup is a rectangle
300
-
301
- :return: True is the selection is a rectangle, False otherwise
302
- """
303
- return (
304
- len(self._selection_boxes) == 1
305
- or len(self.merge_boxes().selection_boxes) == 1
306
- )
307
-
308
- @property
309
- def selection_boxes(self) -> Tuple[SelectionBox, ...]:
310
- """
311
- A tuple of the :class:`SelectionBox` instances stored for this group.
312
- """
313
- return self._selection_boxes
314
-
315
- @property
316
- def selection_boxes_sorted(self) -> List[SelectionBox]:
317
- """
318
- A list of the :class:`SelectionBox` instances for this group sorted by their hash.
319
- """
320
- return sorted(self._selection_boxes, key=hash)
321
-
322
- def chunk_count(self, sub_chunk_size: int = 16) -> int:
323
- return len(self.chunk_locations(sub_chunk_size))
324
-
325
- def chunk_locations(self, sub_chunk_size: int = 16) -> Set[ChunkCoordinates]:
326
- return set(
327
- location
328
- for box in self.selection_boxes
329
- for location in box.chunk_locations(sub_chunk_size)
330
- )
331
-
332
- def chunk_boxes(
333
- self, sub_chunk_size: int = 16
334
- ) -> Iterable[Tuple[ChunkCoordinates, SelectionBox]]:
335
- for box in self.selection_boxes:
336
- yield from box.chunk_boxes(sub_chunk_size)
337
-
338
- def sub_chunk_count(self, sub_chunk_size: int = 16) -> int:
339
- return len(self.sub_chunk_locations(sub_chunk_size))
340
-
341
- def sub_chunk_locations(self, sub_chunk_size: int = 16) -> Set[SubChunkCoordinates]:
342
- yield from set(
343
- location
344
- for box in self.selection_boxes
345
- for location in box.sub_chunk_locations(sub_chunk_size)
346
- )
347
-
348
- def sub_chunk_boxes(
349
- self, sub_chunk_size: int = 16
350
- ) -> Iterable[Tuple[SubChunkCoordinates, SelectionBox]]:
351
- for box in self.selection_boxes:
352
- for (cx, cy, cz), sub_box in box.sub_chunk_boxes(sub_chunk_size):
353
- yield (cx, cy, cz), box
354
-
355
- def intersects(self, other: Union[SelectionGroup, SelectionBox]) -> bool:
356
- if isinstance(other, SelectionGroup):
357
- return any(
358
- self_box.intersects(other_box)
359
- for self_box in self.selection_boxes
360
- for other_box in other.selection_boxes
361
- )
362
- elif isinstance(other, SelectionBox):
363
- return any(self_box.intersects(other) for self_box in self.selection_boxes)
364
-
365
- @staticmethod
366
- def _to_group(other: Union[SelectionGroup, SelectionBox]) -> SelectionGroup:
367
- if not isinstance(other, (SelectionGroup, SelectionBox)):
368
- raise TypeError("other must be a SelectionGroup or SelectionBox.")
369
- if isinstance(other, SelectionBox):
370
- other = SelectionGroup(other)
371
- return other
372
-
373
- def intersection(
374
- self, other: Union[SelectionGroup, SelectionBox]
375
- ) -> SelectionGroup:
376
- other = self._to_group(other)
377
- intersection = []
378
- for self_box in self.selection_boxes:
379
- for other_box in other.selection_boxes:
380
- if self_box.intersects(other_box):
381
- intersection.append(self_box.intersection(other_box))
382
- return SelectionGroup(intersection)
383
-
384
- def subtract(self, other: Union[SelectionGroup, SelectionBox]) -> SelectionGroup:
385
- """
386
- Returns a new :class:`SelectionGroup` containing the volume that does not intersect with other.
387
-
388
- This may be empty if other fully contains self or equal to self if they do not intersect.
389
-
390
- :param other: The :class:`SelectionBox` or :class:`SelectionGroup` to subtract.
391
- """
392
- other = self._to_group(other)
393
- selections = self
394
- for other_box in other:
395
- # for each box in other
396
- selections_new = SelectionGroup()
397
- for self_box in selections:
398
- selections_new += self_box.subtract(other_box)
399
- selections = selections_new
400
- if not selections:
401
- break
402
- return selections
403
-
404
- def union(self, other: Union[SelectionGroup, SelectionBox]) -> SelectionGroup:
405
- """
406
- Returns a new SelectionGroup containing the volume of self and other.
407
-
408
- :param other: The other selection to add to this one.
409
- """
410
- other = self._to_group(other)
411
- if other.is_subset(self):
412
- return self
413
- else:
414
- return self.subtract(other) + other
415
-
416
- def is_subset(self, other: Union[SelectionGroup, SelectionBox]) -> bool:
417
- """
418
- Is this selection completely contained within ``other``.
419
-
420
- :param other: The other selection to test against.
421
- :return: True if this selection completely fits in other.
422
- """
423
- other = self._to_group(other)
424
- return not self.subtract(other)
425
-
426
- def closest_vector_intersection(
427
- self, origin: PointCoordinatesAny, vector: PointCoordinatesAny
428
- ) -> Tuple[Optional[int], float]:
429
- """
430
- Returns the index for the closest box in the look vector and the multiplier of the look vector to get there.
431
-
432
- :param origin: The origin tuple of the vector
433
- :param vector: The vector magnitude in x, y and z
434
- :return: Index for the closest box and the multiplier of the vector to get there. None, inf if no intersection.
435
- """
436
- index_return = None
437
- multiplier = float("inf")
438
- for index, box in enumerate(self._selection_boxes):
439
- mult = box.intersects_vector(origin, vector)
440
- if mult is not None and mult < multiplier:
441
- multiplier = mult
442
- index_return = index
443
- return index_return, multiplier
444
-
445
- def transform(
446
- self, scale: FloatTriplet, rotation: FloatTriplet, translation: FloatTriplet
447
- ) -> SelectionGroup:
448
- """
449
- Creates a new :class:`SelectionGroup` transformed by the given inputs.
450
-
451
- :param scale: A tuple of scaling factors in the x, y and z axis.
452
- :param rotation: The rotation about the x, y and z axis in radians.
453
- :param translation: The translation about the x, y and z axis.
454
- :return: A new :class:`~amulet.api.selection.SelectionGroup` representing the transformed selection.
455
- """
456
- selection_group = SelectionGroup()
457
- for selection in self.selection_boxes:
458
- selection_group += selection.transform(scale, rotation, translation)
459
- return selection_group
460
-
461
- @property
462
- def volume(self) -> int:
463
- return sum(box.volume for box in self.selection_boxes)
464
-
465
- @property
466
- def footprint_area(self) -> int:
467
- """
468
- The 2D area that the selection fills when looking at the selection from above.
469
- """
470
- return (
471
- SelectionGroup(
472
- [
473
- SelectionBox((box.min_x, 0, box.min_z), (box.max_x, 1, box.max_z))
474
- for box in self.selection_boxes
475
- ]
476
- )
477
- .merge_boxes()
478
- .volume
479
- )
480
-
481
-
482
- if __name__ == "__main__":
483
- b1 = SelectionBox((0, 0, 0), (4, 4, 4))
484
- b2 = SelectionBox((7, 7, 7), (10, 10, 10))
485
- sel = SelectionGroup((b1, b2))
486
-
487
- for x, y, z in sel.blocks:
488
- print(x, y, z)
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import numpy
5
+
6
+ from typing import Tuple, Iterable, List, Union, Optional, overload, Set
7
+
8
+ from amulet.api.data_types import (
9
+ BlockCoordinates,
10
+ CoordinatesAny,
11
+ ChunkCoordinates,
12
+ SubChunkCoordinates,
13
+ FloatTriplet,
14
+ PointCoordinatesAny,
15
+ )
16
+ from .abstract_selection import AbstractBaseSelection
17
+ from .box import SelectionBox
18
+
19
+
20
+ class SelectionGroup(AbstractBaseSelection):
21
+ """
22
+ A container for zero or more :class:`SelectionBox` instances.
23
+
24
+ This allows for non-rectangular and non-contiguous selections.
25
+ """
26
+
27
+ def __init__(
28
+ self, selection_boxes: Union[SelectionBox, Iterable[SelectionBox]] = ()
29
+ ):
30
+ """
31
+ Construct a new :class:`SelectionGroup` class from the given data.
32
+
33
+ >>> SelectionGroup(SelectionBox((0, 0, 0), (1, 1, 1)))
34
+ >>> SelectionGroup([
35
+ >>> SelectionBox((0, 0, 0), (1, 1, 1)),
36
+ >>> SelectionBox((1, 1, 1), (2, 2, 2))
37
+ >>> ])
38
+
39
+ :param selection_boxes: A :class:`SelectionBox` or iterable of :class:`SelectionBox` classes.
40
+ """
41
+ if isinstance(selection_boxes, SelectionBox):
42
+ self._selection_boxes: Tuple[SelectionBox, ...] = (selection_boxes,)
43
+ else:
44
+ self._selection_boxes: Tuple[SelectionBox, ...] = tuple(
45
+ box for box in selection_boxes if isinstance(box, SelectionBox)
46
+ )
47
+
48
+ def __repr__(self) -> str:
49
+ boxes = ", ".join([repr(box) for box in self.selection_boxes])
50
+ return f"SelectionGroup([{boxes}])"
51
+
52
+ def __str__(self) -> str:
53
+ boxes = ", ".join([str(box) for box in self.selection_boxes])
54
+ return f"[{boxes}]"
55
+
56
+ def __eq__(self, other: SelectionGroup) -> bool:
57
+ """
58
+ Does the contents of this :class:`SelectionGroup` match the other :class:`SelectionGroup`.
59
+
60
+ Note if the boxes do not exactly match this will return False even if the volume represented is the same.
61
+
62
+ :param other: The other :class:`SelectionGroup` to compare with.
63
+ :return: True if the boxes contained match.
64
+ """
65
+ return (
66
+ isinstance(other, SelectionGroup)
67
+ and self.selection_boxes_sorted == other.selection_boxes_sorted
68
+ )
69
+
70
+ def __add__(self, boxes: Iterable[SelectionBox]) -> SelectionGroup:
71
+ """
72
+ Add an iterable of :class:`SelectionBox` classes to this :class:`SelectionGroup`.
73
+
74
+ Note this will construct a new :class:`SelectionGroup` because it is immutable so cannot be modified in place.
75
+
76
+ >>> group1 = SelectionGroup(SelectionBox((-1, -1, -1), (0, 0, 0)))
77
+ >>> group2 = SelectionGroup([
78
+ >>> SelectionBox((0, 0, 0), (1, 1, 1)),
79
+ >>> SelectionBox((1, 1, 1), (2, 2, 2))
80
+ >>> ])
81
+ >>> group1 + group2
82
+ SelectionGroup([SelectionBox((-1, -1, -1), (0, 0, 0)), SelectionBox((0, 0, 0), (1, 1, 1)), SelectionBox((1, 1, 1), (2, 2, 2))])
83
+ >>> group1 += group2
84
+ >>> group1
85
+ SelectionGroup([SelectionBox((-1, -1, -1), (0, 0, 0)), SelectionBox((0, 0, 0), (1, 1, 1)), SelectionBox((1, 1, 1), (2, 2, 2))])
86
+
87
+ :param boxes: An iterable of boxes to add to this group.
88
+ :return: A new :class:`SelectionGroup` class containing the boxes from this instance and those in ``boxes``.
89
+ """
90
+ try:
91
+ boxes = tuple(boxes)
92
+ except:
93
+ raise NotImplemented
94
+ if all(isinstance(b, SelectionBox) for b in boxes):
95
+ return SelectionGroup(tuple(self) + boxes)
96
+ raise NotImplemented
97
+
98
+ def __iter__(self) -> Iterable[SelectionBox]:
99
+ """An iterable of all the :class:`SelectionBox` classes in the group."""
100
+ yield from self._selection_boxes
101
+
102
+ def __len__(self) -> int:
103
+ """The number of :class:`SelectionBox` classes in the group."""
104
+ return len(self._selection_boxes)
105
+
106
+ def __contains__(self, item: CoordinatesAny) -> bool:
107
+ return self.contains_block(item)
108
+
109
+ def contains_block(self, coords: CoordinatesAny) -> bool:
110
+ return any(box.contains_block(coords) for box in self._selection_boxes)
111
+
112
+ def contains_point(self, coords: CoordinatesAny) -> bool:
113
+ return any(box.contains_point(coords) for box in self._selection_boxes)
114
+
115
+ @property
116
+ def blocks(self) -> Iterable[BlockCoordinates]:
117
+ """
118
+ The location of every block in the selection.
119
+
120
+ >>> for x, y, z in group.blocks:
121
+ >>> ...
122
+
123
+ Note: if boxes intersect, the blocks in the intersected region will be included multiple times.
124
+
125
+ If this behaviour is not desired the :meth:`merge_boxes` method will return a new SelectionGroup with no intersections.
126
+
127
+ >>> for x, y, z in group.merge_boxes().blocks:
128
+ >>> ...
129
+
130
+ :return: An iterable of block locations.
131
+ """
132
+ return itertools.chain.from_iterable(self.selection_boxes)
133
+
134
+ def __bool__(self) -> bool:
135
+ """Are there any boxes in the group."""
136
+ return bool(self._selection_boxes)
137
+
138
+ @overload
139
+ def __getitem__(self, item: int) -> SelectionBox:
140
+ ...
141
+
142
+ @overload
143
+ def __getitem__(self, item: slice) -> SelectionGroup:
144
+ ...
145
+
146
+ def __getitem__(self, item):
147
+ """Get the selection box at the given index."""
148
+ val = self._selection_boxes[item]
149
+ if isinstance(val, tuple):
150
+ return SelectionGroup(val)
151
+ else:
152
+ return val
153
+
154
+ @property
155
+ def min(self) -> BlockCoordinates:
156
+ """The minimum point of of all the boxes in the group."""
157
+ return tuple(self.min_array.tolist())
158
+
159
+ @property
160
+ def min_array(self) -> numpy.ndarray:
161
+ """The minimum point of of all the boxes in the group as a numpy array."""
162
+ if self._selection_boxes:
163
+ return numpy.min(numpy.array([box.min for box in self._selection_boxes]), 0)
164
+ else:
165
+ raise ValueError("SelectionGroup does not contain any SelectionBoxes")
166
+
167
+ @property
168
+ def min_x(self) -> int:
169
+ return int(self.min_array[0])
170
+
171
+ @property
172
+ def min_y(self) -> int:
173
+ return int(self.min_array[1])
174
+
175
+ @property
176
+ def min_z(self) -> int:
177
+ return int(self.min_array[2])
178
+
179
+ @property
180
+ def max(self) -> BlockCoordinates:
181
+ """The maximum point of of all the boxes in the group."""
182
+ return tuple(self.max_array.tolist())
183
+
184
+ @property
185
+ def max_array(self) -> numpy.ndarray:
186
+ """The maximum point of of all the boxes in the group as a numpy array."""
187
+ if self._selection_boxes:
188
+ return numpy.max(numpy.array([box.max for box in self._selection_boxes]), 0)
189
+ else:
190
+ raise ValueError("SelectionGroup does not contain any SelectionBoxes")
191
+
192
+ @property
193
+ def max_x(self) -> int:
194
+ return int(self.max_array[0])
195
+
196
+ @property
197
+ def max_y(self) -> int:
198
+ return int(self.max_array[1])
199
+
200
+ @property
201
+ def max_z(self) -> int:
202
+ return int(self.max_array[2])
203
+
204
+ @property
205
+ def bounds(self) -> Tuple[BlockCoordinates, BlockCoordinates]:
206
+ return self.min, self.max
207
+
208
+ @property
209
+ def bounds_array(self) -> numpy.ndarray:
210
+ return numpy.array([self.min_array, self.max_array])
211
+
212
+ def to_box(self) -> SelectionBox:
213
+ """Create a `SelectionBox` based off the bounds of the boxes in the group."""
214
+ return SelectionBox(self.min, self.max)
215
+
216
+ def merge_boxes(self) -> SelectionGroup:
217
+ """
218
+ Take the boxes as they were given to this class, merge neighbouring boxes and remove overlapping regions.
219
+
220
+ The result should be a SelectionGroup containing one or more SelectionBox classes that represents the same
221
+ volume as the original but with no overlapping boxes.
222
+ """
223
+ selection_boxes = self.selection_boxes
224
+ # TODO remove duplicate boxes
225
+
226
+ # remove duplicates
227
+ selection_boxes_ = []
228
+ for box in selection_boxes:
229
+ if not any(box == box_ for box_ in selection_boxes_):
230
+ selection_boxes_.append(box)
231
+ selection_boxes = selection_boxes_
232
+
233
+ if len(selection_boxes) >= 2:
234
+ merge_boxes = True
235
+ while merge_boxes:
236
+ # find two neighbouring boxes and merge them
237
+ merge_boxes = False # if two boxes get merged this will be set back to True and this will run again.
238
+ box_index = 0 # the index of the first box
239
+ while box_index < len(selection_boxes):
240
+ box = selection_boxes[box_index]
241
+ other_index = box_index + 1 # the index of the second box.
242
+ # This always starts at one greater than box_index because
243
+ # the lower values were already checked the other way around
244
+ while other_index < len(selection_boxes):
245
+ other = selection_boxes[other_index]
246
+ x_dim = box.min_x == other.min_x and box.max_x == other.max_x
247
+ y_dim = box.min_y == other.min_y and box.max_y == other.max_y
248
+ z_dim = box.min_z == other.min_z and box.max_z == other.max_z
249
+
250
+ x_border = box.max_x == other.min_x or other.max_x == box.min_x
251
+ y_border = box.max_y == other.min_y or other.max_y == box.min_y
252
+ z_border = box.max_z == other.min_z or other.max_z == box.min_z
253
+
254
+ if (
255
+ (x_dim and y_dim and z_border)
256
+ or (x_dim and z_dim and y_border)
257
+ or (y_dim and z_dim and x_border)
258
+ ):
259
+ selection_boxes.pop(other_index)
260
+ selection_boxes.pop(box_index)
261
+ selection_boxes.append(
262
+ SelectionBox(
263
+ numpy.min([box.min, other.min], 0),
264
+ numpy.max([box.max, other.max], 0),
265
+ )
266
+ )
267
+ merge_boxes = True
268
+ box = selection_boxes[box_index]
269
+ other_index = box_index + 1
270
+ else:
271
+ other_index += 1
272
+ box_index += 1
273
+ return SelectionGroup(selection_boxes)
274
+
275
+ @property
276
+ def is_contiguous(self) -> bool:
277
+ """
278
+ Does the SelectionGroup represent one connected region (True) or multiple separated regions (False).
279
+
280
+ If two boxes are touching at the corners this is classed as contiguous.
281
+ """
282
+ # TODO: This needs some work. It will only work if the selections touch in a chain
283
+ # it does not care if it loops back and intersects itself. Does intersecting count as being contiguous?
284
+ # I would say yes
285
+ if len(self._selection_boxes) == 1:
286
+ return True
287
+
288
+ for i in range(len(self._selection_boxes) - 1):
289
+ sub_box: SelectionBox = self._selection_boxes[i]
290
+ next_box: SelectionBox = self._selection_boxes[i + 1]
291
+ if not sub_box.touches(next_box):
292
+ return False
293
+
294
+ return True
295
+
296
+ @property
297
+ def is_rectangular(self) -> bool:
298
+ """
299
+ Checks if the SelectionGroup is a rectangle
300
+
301
+ :return: True is the selection is a rectangle, False otherwise
302
+ """
303
+ return (
304
+ len(self._selection_boxes) == 1
305
+ or len(self.merge_boxes().selection_boxes) == 1
306
+ )
307
+
308
+ @property
309
+ def selection_boxes(self) -> Tuple[SelectionBox, ...]:
310
+ """
311
+ A tuple of the :class:`SelectionBox` instances stored for this group.
312
+ """
313
+ return self._selection_boxes
314
+
315
+ @property
316
+ def selection_boxes_sorted(self) -> List[SelectionBox]:
317
+ """
318
+ A list of the :class:`SelectionBox` instances for this group sorted by their hash.
319
+ """
320
+ return sorted(self._selection_boxes, key=hash)
321
+
322
+ def chunk_count(self, sub_chunk_size: int = 16) -> int:
323
+ return len(self.chunk_locations(sub_chunk_size))
324
+
325
+ def chunk_locations(self, sub_chunk_size: int = 16) -> Set[ChunkCoordinates]:
326
+ return set(
327
+ location
328
+ for box in self.selection_boxes
329
+ for location in box.chunk_locations(sub_chunk_size)
330
+ )
331
+
332
+ def chunk_boxes(
333
+ self, sub_chunk_size: int = 16
334
+ ) -> Iterable[Tuple[ChunkCoordinates, SelectionBox]]:
335
+ for box in self.selection_boxes:
336
+ yield from box.chunk_boxes(sub_chunk_size)
337
+
338
+ def sub_chunk_count(self, sub_chunk_size: int = 16) -> int:
339
+ return len(self.sub_chunk_locations(sub_chunk_size))
340
+
341
+ def sub_chunk_locations(self, sub_chunk_size: int = 16) -> Set[SubChunkCoordinates]:
342
+ yield from set(
343
+ location
344
+ for box in self.selection_boxes
345
+ for location in box.sub_chunk_locations(sub_chunk_size)
346
+ )
347
+
348
+ def sub_chunk_boxes(
349
+ self, sub_chunk_size: int = 16
350
+ ) -> Iterable[Tuple[SubChunkCoordinates, SelectionBox]]:
351
+ for box in self.selection_boxes:
352
+ for (cx, cy, cz), sub_box in box.sub_chunk_boxes(sub_chunk_size):
353
+ yield (cx, cy, cz), box
354
+
355
+ def intersects(self, other: Union[SelectionGroup, SelectionBox]) -> bool:
356
+ if isinstance(other, SelectionGroup):
357
+ return any(
358
+ self_box.intersects(other_box)
359
+ for self_box in self.selection_boxes
360
+ for other_box in other.selection_boxes
361
+ )
362
+ elif isinstance(other, SelectionBox):
363
+ return any(self_box.intersects(other) for self_box in self.selection_boxes)
364
+
365
+ @staticmethod
366
+ def _to_group(other: Union[SelectionGroup, SelectionBox]) -> SelectionGroup:
367
+ if not isinstance(other, (SelectionGroup, SelectionBox)):
368
+ raise TypeError("other must be a SelectionGroup or SelectionBox.")
369
+ if isinstance(other, SelectionBox):
370
+ other = SelectionGroup(other)
371
+ return other
372
+
373
+ def intersection(
374
+ self, other: Union[SelectionGroup, SelectionBox]
375
+ ) -> SelectionGroup:
376
+ other = self._to_group(other)
377
+ intersection = []
378
+ for self_box in self.selection_boxes:
379
+ for other_box in other.selection_boxes:
380
+ if self_box.intersects(other_box):
381
+ intersection.append(self_box.intersection(other_box))
382
+ return SelectionGroup(intersection)
383
+
384
+ def subtract(self, other: Union[SelectionGroup, SelectionBox]) -> SelectionGroup:
385
+ """
386
+ Returns a new :class:`SelectionGroup` containing the volume that does not intersect with other.
387
+
388
+ This may be empty if other fully contains self or equal to self if they do not intersect.
389
+
390
+ :param other: The :class:`SelectionBox` or :class:`SelectionGroup` to subtract.
391
+ """
392
+ other = self._to_group(other)
393
+ selections = self
394
+ for other_box in other:
395
+ # for each box in other
396
+ selections_new = SelectionGroup()
397
+ for self_box in selections:
398
+ selections_new += self_box.subtract(other_box)
399
+ selections = selections_new
400
+ if not selections:
401
+ break
402
+ return selections
403
+
404
+ def union(self, other: Union[SelectionGroup, SelectionBox]) -> SelectionGroup:
405
+ """
406
+ Returns a new SelectionGroup containing the volume of self and other.
407
+
408
+ :param other: The other selection to add to this one.
409
+ """
410
+ other = self._to_group(other)
411
+ if other.is_subset(self):
412
+ return self
413
+ else:
414
+ return self.subtract(other) + other
415
+
416
+ def is_subset(self, other: Union[SelectionGroup, SelectionBox]) -> bool:
417
+ """
418
+ Is this selection completely contained within ``other``.
419
+
420
+ :param other: The other selection to test against.
421
+ :return: True if this selection completely fits in other.
422
+ """
423
+ other = self._to_group(other)
424
+ return not self.subtract(other)
425
+
426
+ def closest_vector_intersection(
427
+ self, origin: PointCoordinatesAny, vector: PointCoordinatesAny
428
+ ) -> Tuple[Optional[int], float]:
429
+ """
430
+ Returns the index for the closest box in the look vector and the multiplier of the look vector to get there.
431
+
432
+ :param origin: The origin tuple of the vector
433
+ :param vector: The vector magnitude in x, y and z
434
+ :return: Index for the closest box and the multiplier of the vector to get there. None, inf if no intersection.
435
+ """
436
+ index_return = None
437
+ multiplier = float("inf")
438
+ for index, box in enumerate(self._selection_boxes):
439
+ mult = box.intersects_vector(origin, vector)
440
+ if mult is not None and mult < multiplier:
441
+ multiplier = mult
442
+ index_return = index
443
+ return index_return, multiplier
444
+
445
+ def transform(
446
+ self, scale: FloatTriplet, rotation: FloatTriplet, translation: FloatTriplet
447
+ ) -> SelectionGroup:
448
+ """
449
+ Creates a new :class:`SelectionGroup` transformed by the given inputs.
450
+
451
+ :param scale: A tuple of scaling factors in the x, y and z axis.
452
+ :param rotation: The rotation about the x, y and z axis in radians.
453
+ :param translation: The translation about the x, y and z axis.
454
+ :return: A new :class:`~amulet.api.selection.SelectionGroup` representing the transformed selection.
455
+ """
456
+ selection_group = SelectionGroup()
457
+ for selection in self.selection_boxes:
458
+ selection_group += selection.transform(scale, rotation, translation)
459
+ return selection_group
460
+
461
+ @property
462
+ def volume(self) -> int:
463
+ return sum(box.volume for box in self.selection_boxes)
464
+
465
+ @property
466
+ def footprint_area(self) -> int:
467
+ """
468
+ The 2D area that the selection fills when looking at the selection from above.
469
+ """
470
+ return (
471
+ SelectionGroup(
472
+ [
473
+ SelectionBox((box.min_x, 0, box.min_z), (box.max_x, 1, box.max_z))
474
+ for box in self.selection_boxes
475
+ ]
476
+ )
477
+ .merge_boxes()
478
+ .volume
479
+ )
480
+
481
+
482
+ if __name__ == "__main__":
483
+ b1 = SelectionBox((0, 0, 0), (4, 4, 4))
484
+ b2 = SelectionBox((7, 7, 7), (10, 10, 10))
485
+ sel = SelectionGroup((b1, b2))
486
+
487
+ for x, y, z in sel.blocks:
488
+ print(x, y, z)