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,805 +1,805 @@
1
- from __future__ import annotations
2
-
3
- import itertools
4
- import numpy
5
- import math
6
-
7
- from typing import Tuple, Iterable, Generator, Optional, Union
8
-
9
- from amulet.api.data_types import (
10
- BlockCoordinates,
11
- BlockCoordinatesAny,
12
- CoordinatesAny,
13
- ChunkCoordinates,
14
- SubChunkCoordinates,
15
- FloatTriplet,
16
- PointCoordinatesAny,
17
- )
18
- from amulet.utils.world_utils import (
19
- block_coords_to_chunk_coords,
20
- blocks_slice_to_chunk_slice,
21
- )
22
- from amulet.utils.matrix import (
23
- transform_matrix,
24
- displacement_matrix,
25
- )
26
- from .abstract_selection import AbstractBaseSelection
27
- from .. import selection
28
-
29
-
30
- class SelectionBox(AbstractBaseSelection):
31
- """
32
- The SelectionBox class represents a single cuboid selection.
33
-
34
- When combined with :class:`~amulet.api.selection.SelectionGroup` it can represent any arbitrary shape.
35
- """
36
-
37
- __slots__ = (
38
- "_min_x",
39
- "_min_y",
40
- "_min_z",
41
- "_max_x",
42
- "_max_y",
43
- "_max_z",
44
- "_point_1",
45
- "_point_2",
46
- )
47
-
48
- def __init__(self, point_1: BlockCoordinatesAny, point_2: BlockCoordinatesAny):
49
- """
50
- Construct a new SelectionBox instance.
51
-
52
- >>> # a selection box that selects one block.
53
- >>> box = SelectionBox(
54
- >>> (0, 0, 0),
55
- >>> (1, 1, 1)
56
- >>> )
57
-
58
- :param point_1: The first point of the selection.
59
- :param point_2: The second point of the selection.
60
- """
61
- box = numpy.array([point_1, point_2]).round().astype(int)
62
- p1, p2 = box.tolist()
63
- self._point_1 = tuple(p1)
64
- self._point_2 = tuple(p2)
65
- self._min_x, self._min_y, self._min_z = numpy.min(box, 0).tolist()
66
- self._max_x, self._max_y, self._max_z = numpy.max(box, 0).tolist()
67
-
68
- @classmethod
69
- def create_chunk_box(
70
- cls, cx: int, cz: int, sub_chunk_size: int = 16
71
- ) -> SelectionBox:
72
- """
73
- Get a :class:`SelectionBox` containing the whole of a given chunk.
74
-
75
- >>> box = SelectionBox.create_chunk_box(1, 2)
76
- SelectionBox((16, -1073741824, 32), (32, 1073741824, 48))
77
-
78
- :param cx: The x coordinate of the chunk
79
- :param cz: The z coordinate of the chunk
80
- :param sub_chunk_size: The dimension of a sub-chunk. Default 16.
81
- """
82
- return cls(
83
- (cx * sub_chunk_size, -(2**30), cz * sub_chunk_size),
84
- ((cx + 1) * sub_chunk_size, 2**30, (cz + 1) * sub_chunk_size),
85
- )
86
-
87
- @classmethod
88
- def create_sub_chunk_box(
89
- cls, cx: int, cy: int, cz: int, sub_chunk_size: int = 16
90
- ) -> SelectionBox:
91
- """
92
- Get a :class:`SelectionBox` containing the whole of a given sub-chunk.
93
-
94
- >>> SelectionBox.create_sub_chunk_box(1, 0, 2)
95
- SelectionBox((16, 0, 32), (32, 16, 48))
96
-
97
- :param cx: The x coordinate of the chunk
98
- :param cy: The y coordinate of the chunk
99
- :param cz: The z coordinate of the chunk
100
- :param sub_chunk_size: The dimension of a sub-chunk. Default 16.
101
- """
102
- return cls(
103
- (cx * sub_chunk_size, cy * sub_chunk_size, cz * sub_chunk_size),
104
- (
105
- (cx + 1) * sub_chunk_size,
106
- (cy + 1) * sub_chunk_size,
107
- (cz + 1) * sub_chunk_size,
108
- ),
109
- )
110
-
111
- def create_moved_box(
112
- self, offset: BlockCoordinatesAny, subtract=False
113
- ) -> SelectionBox:
114
- """
115
- Create a new :class:`SelectionBox` based on this one with the coordinates moved by the given offset.
116
-
117
- :param offset: The amount to move the box.
118
- :param subtract: If true will subtract the offset rather than adding.
119
- :return: The new selection with the given offset.
120
- """
121
- offset = numpy.array(offset)
122
- if subtract:
123
- offset *= -1
124
- return SelectionBox(offset + self.min, offset + self.max)
125
-
126
- def chunk_locations(self, sub_chunk_size: int = 16) -> Iterable[ChunkCoordinates]:
127
- cx_min, cz_min, cx_max, cz_max = block_coords_to_chunk_coords(
128
- self.min_x,
129
- self.min_z,
130
- self.max_x - 1,
131
- self.max_z - 1,
132
- sub_chunk_size=sub_chunk_size,
133
- )
134
- yield from itertools.product(
135
- range(cx_min, cx_max + 1), range(cz_min, cz_max + 1)
136
- )
137
-
138
- def chunk_boxes(
139
- self, sub_chunk_size: int = 16
140
- ) -> Iterable[Tuple[ChunkCoordinates, SelectionBox]]:
141
- for cx, cz in self.chunk_locations(sub_chunk_size):
142
- yield (cx, cz), self.intersection(
143
- SelectionBox.create_chunk_box(cx, cz, sub_chunk_size)
144
- )
145
-
146
- def chunk_y_locations(self, sub_chunk_size: int = 16) -> Iterable[int]:
147
- """
148
- An iterable of all the sub-chunk y indexes this box intersects.
149
-
150
- :param sub_chunk_size: The dimension of a sub-chunk. Default 16.
151
- """
152
- cy_min, cy_max = block_coords_to_chunk_coords(
153
- self.min_y, self._max_y - 1, sub_chunk_size=sub_chunk_size
154
- )
155
- for cy in range(cy_min, cy_max + 1):
156
- yield cy
157
-
158
- def sub_chunk_locations(
159
- self, sub_chunk_size: int = 16
160
- ) -> Iterable[SubChunkCoordinates]:
161
- for cx, cz in self.chunk_locations(sub_chunk_size):
162
- for cy in self.chunk_y_locations(sub_chunk_size):
163
- yield cx, cy, cz
164
-
165
- def chunk_count(self, sub_chunk_size: int = 16) -> int:
166
- cx_min, cz_min, cx_max, cz_max = block_coords_to_chunk_coords(
167
- self.min_x,
168
- self.min_z,
169
- self.max_x - 1,
170
- self.max_z - 1,
171
- sub_chunk_size=sub_chunk_size,
172
- )
173
- return (cx_max + 1 - cx_min) * (cz_max + 1 - cz_min)
174
-
175
- def sub_chunk_count(self, sub_chunk_size: int = 16) -> int:
176
- cy_min, cy_max = block_coords_to_chunk_coords(
177
- self.min_y,
178
- self.max_y - 1,
179
- sub_chunk_size=sub_chunk_size,
180
- )
181
- return (cy_max + 1 - cy_min) * self.chunk_count()
182
-
183
- def sub_chunk_boxes(
184
- self, sub_chunk_size: int = 16
185
- ) -> Iterable[Tuple[SubChunkCoordinates, SelectionBox]]:
186
- for cx, cy, cz in self.sub_chunk_locations(sub_chunk_size):
187
- yield (cx, cy, cz), self.intersection(
188
- SelectionBox.create_sub_chunk_box(cx, cy, cz, sub_chunk_size)
189
- )
190
-
191
- def __iter__(self) -> Iterable[BlockCoordinates]:
192
- """An iterable of all the block locations within this box."""
193
- return self.blocks
194
-
195
- @property
196
- def blocks(self) -> Iterable[BlockCoordinates]:
197
- return itertools.product(
198
- range(self._min_x, self._max_x),
199
- range(self._min_y, self._max_y),
200
- range(self._min_z, self._max_z),
201
- )
202
-
203
- def __repr__(self) -> str:
204
- return f"SelectionBox({self.point_1}, {self.point_2})"
205
-
206
- def __str__(self) -> str:
207
- return f"({self.point_1}, {self.point_2})"
208
-
209
- def __contains__(self, item: CoordinatesAny) -> bool:
210
- return self.contains_block(item)
211
-
212
- def contains_block(self, coords: CoordinatesAny) -> bool:
213
- return (
214
- self._min_x <= coords[0] < self._max_x
215
- and self._min_y <= coords[1] < self._max_y
216
- and self._min_z <= coords[2] < self._max_z
217
- )
218
-
219
- def contains_point(self, coords: CoordinatesAny) -> bool:
220
- return (
221
- self._min_x <= coords[0] <= self._max_x
222
- and self._min_y <= coords[1] <= self._max_y
223
- and self._min_z <= coords[2] <= self._max_z
224
- )
225
-
226
- def __eq__(self, other) -> bool:
227
- return self.min == other.min and self.max == other.max
228
-
229
- def __ne__(self, other) -> bool:
230
- return not self == other
231
-
232
- def __hash__(self) -> int:
233
- return hash((*self.min, *self.max))
234
-
235
- @property
236
- def slice(self) -> Tuple[slice, slice, slice]:
237
- """
238
- Converts the :class:`SelectionBox` minimum/maximum coordinates into slice arguments
239
-
240
- :return: The :class:`SelectionBox` coordinates as slices in (x,y,z) order
241
- """
242
- return (
243
- slice(self._min_x, self._max_x),
244
- slice(self._min_y, self._max_y),
245
- slice(self._min_z, self._max_z),
246
- )
247
-
248
- def chunk_slice(
249
- self, cx: int, cz: int, sub_chunk_size: int = 16
250
- ) -> Tuple[slice, slice, slice]:
251
- """
252
- Get the slice of the box in relative form for a given chunk.
253
-
254
- >>> SelectionBox((0, 0, 0), (32, 32, 32)).chunk_slice(1, 1)
255
- (slice(0, 16, None), slice(0, 32, None), slice(0, 16, None))
256
-
257
- :param cx: The x coordinate of the chunk
258
- :param cz: The z coordinate of the chunk
259
- :param sub_chunk_size: The dimension of a sub-chunk. Default 16.
260
- """
261
- s_x, s_y, s_z = self.slice
262
- x_chunk_slice = blocks_slice_to_chunk_slice(s_x, sub_chunk_size, cx)
263
- z_chunk_slice = blocks_slice_to_chunk_slice(s_z, sub_chunk_size, cz)
264
- return x_chunk_slice, s_y, z_chunk_slice
265
-
266
- def sub_chunk_slice(
267
- self, cx: int, cy: int, cz: int, sub_chunk_size: int = 16
268
- ) -> Tuple[slice, slice, slice]:
269
- """
270
- Get the slice of the box in relative form for a given sub-chunk.
271
-
272
- >>> SelectionBox((0, 0, 0), (32, 32, 32)).sub_chunk_slice(1, 1, 1)
273
- (slice(0, 16, None), slice(0, 16, None), slice(0, 16, None))
274
-
275
- :param cx: The x coordinate of the chunk
276
- :param cy: The y coordinate of the chunk
277
- :param cz: The z coordinate of the chunk
278
- :param sub_chunk_size: The dimension of a sub-chunk. Default 16.
279
- """
280
- x_chunk_slice, s_y, z_chunk_slice = self.chunk_slice(cx, cz, sub_chunk_size)
281
- y_chunk_slice = blocks_slice_to_chunk_slice(s_y, sub_chunk_size, cy)
282
- return x_chunk_slice, y_chunk_slice, z_chunk_slice
283
-
284
- @property
285
- def point_1(self) -> BlockCoordinates:
286
- """The first value given to the constructor."""
287
- return self._point_1
288
-
289
- @property
290
- def point_2(self) -> BlockCoordinates:
291
- """The second value given to the constructor."""
292
- return self._point_2
293
-
294
- @property
295
- def points(self) -> Tuple[BlockCoordinates, BlockCoordinates]:
296
- """The points given to the constructor."""
297
- return self.point_1, self.point_2
298
-
299
- @property
300
- def points_array(self) -> numpy.ndarray:
301
- """The points given to the constructor as a numpy array."""
302
- return numpy.array(self.points)
303
-
304
- @property
305
- def min_x(self) -> int:
306
- return self._min_x
307
-
308
- @property
309
- def min_y(self) -> int:
310
- return self._min_y
311
-
312
- @property
313
- def min_z(self) -> int:
314
- return self._min_z
315
-
316
- @property
317
- def max_x(self) -> int:
318
- return self._max_x
319
-
320
- @property
321
- def max_y(self) -> int:
322
- return self._max_y
323
-
324
- @property
325
- def max_z(self) -> int:
326
- return self._max_z
327
-
328
- @property
329
- def min(self) -> BlockCoordinates:
330
- return self._min_x, self._min_y, self._min_z
331
-
332
- @property
333
- def min_array(self) -> numpy.ndarray:
334
- return numpy.array(self.min)
335
-
336
- @property
337
- def max(self) -> BlockCoordinates:
338
- return self._max_x, self._max_y, self._max_z
339
-
340
- @property
341
- def max_array(self) -> numpy.ndarray:
342
- return numpy.array(self.max)
343
-
344
- @property
345
- def bounds(self) -> Tuple[BlockCoordinates, BlockCoordinates]:
346
- return (
347
- (self._min_x, self._min_y, self._min_z),
348
- (self._max_x, self._max_y, self._max_z),
349
- )
350
-
351
- @property
352
- def bounds_array(self) -> numpy.ndarray:
353
- return numpy.array(self.bounds)
354
-
355
- @property
356
- def size_x(self) -> int:
357
- """The length of the box in the x axis."""
358
- return self._max_x - self._min_x
359
-
360
- @property
361
- def size_y(self) -> int:
362
- """The length of the box in the y axis."""
363
- return self._max_y - self._min_y
364
-
365
- @property
366
- def size_z(self) -> int:
367
- """The length of the box in the z axis."""
368
- return self._max_z - self._min_z
369
-
370
- @property
371
- def shape(self) -> Tuple[int, int, int]:
372
- """
373
- The shape of the box.
374
-
375
- >>> SelectionBox((0, 0, 0), (1, 1, 1)).shape
376
- (1, 1, 1)
377
- """
378
- return self.size_x, self.size_y, self.size_z
379
-
380
- @property
381
- def volume(self) -> int:
382
- """
383
- The number of blocks in the box.
384
-
385
- >>> SelectionBox((0, 0, 0), (1, 1, 1)).shape
386
- 1
387
- """
388
- return self.size_x * self.size_y * self.size_z
389
-
390
- def touches(self, other: SelectionBox) -> bool:
391
- """
392
- Method to check if this instance of :class:`SelectionBox` touches but does not intersect another SelectionBox.
393
-
394
- :param other: The other SelectionBox
395
- :return: True if the two :class:`SelectionBox` instances touch, False otherwise
396
- """
397
- # It touches if the box does not intersect but intersects when expanded by one block.
398
- # There may be a simpler way to do this.
399
- return self.touches_or_intersects(other) and not self.intersects(other)
400
-
401
- def touches_or_intersects(self, other: SelectionBox) -> bool:
402
- """
403
- Method to check if this instance of SelectionBox touches or intersects another SelectionBox.
404
-
405
- :param other: The other SelectionBox.
406
- :return: True if the two :class:`SelectionBox` instances touch or intersect, False otherwise.
407
- """
408
- return not (
409
- self.min_x >= other.max_x + 1
410
- or self.min_y >= other.max_y + 1
411
- or self.min_z >= other.max_z + 1
412
- or self.max_x <= other.min_x - 1
413
- or self.max_y <= other.min_y - 1
414
- or self.max_z <= other.min_z - 1
415
- )
416
-
417
- def intersects(self, other: SelectionBox) -> bool:
418
- """
419
- Method to check whether this instance of SelectionBox intersects another SelectionBox.
420
-
421
- :param other: The other SelectionBox to check for intersection.
422
- :return: True if the two :class:`SelectionBox` instances intersect, False otherwise.
423
- """
424
- return not (
425
- self.min_x >= other.max_x
426
- or self.min_y >= other.max_y
427
- or self.min_z >= other.max_z
428
- or self.max_x <= other.min_x
429
- or self.max_y <= other.min_y
430
- or self.max_z <= other.min_z
431
- )
432
-
433
- def contains_box(self, other: SelectionBox) -> bool:
434
- """
435
- Method to check if the other SelectionBox other fits entirely within this instance of SelectionBox.
436
-
437
- :param other: The SelectionBox to test.
438
- :return: True if other fits with self, False otherwise.
439
- """
440
- return (
441
- self.min_x <= other.min_x
442
- and self.min_y <= other.min_y
443
- and self.min_z <= other.min_z
444
- and other.max_x <= self.max_x
445
- and other.max_y <= self.max_y
446
- and other.max_z <= self.max_z
447
- )
448
-
449
- def intersection(self, other: SelectionBox) -> SelectionBox:
450
- return SelectionBox(
451
- numpy.clip(other.min, self.min, self.max),
452
- numpy.clip(other.max, self.min, self.max),
453
- )
454
-
455
- def subtract(self, other: SelectionBox) -> selection.SelectionGroup:
456
- """
457
- Get a :class:`~amulet.api.selection.SelectionGroup` containing boxes that are in self but not in other.
458
-
459
- This may be empty if other fully contains self or equal to self if they do not intersect.
460
-
461
- :param other: The SelectionBox to subtract.
462
- :return:
463
- """
464
- if self.intersects(other):
465
- other = self.intersection(other)
466
- if self == other:
467
- # if the two selections are the same there is no difference.
468
- return selection.SelectionGroup()
469
- else:
470
- boxes = []
471
- if self.min_y < other.min_y:
472
- # bottom box
473
- boxes.append(
474
- SelectionBox(
475
- (self.min_x, self.min_y, self.min_z),
476
- (self.max_x, other.min_y, self.max_z),
477
- )
478
- )
479
-
480
- if other.max_y < self.max_y:
481
- # top box
482
- boxes.append(
483
- SelectionBox(
484
- (self.min_x, other.max_y, self.min_z),
485
- (self.max_x, self.max_y, self.max_z),
486
- )
487
- )
488
-
489
- # BBB NNN TTT
490
- # BBB WOE TTT
491
- # BBB SSS TTT
492
-
493
- if self.min_z < other.min_z:
494
- # north box
495
- boxes.append(
496
- SelectionBox(
497
- (self.min_x, other.min_y, self.min_z),
498
- (self.max_x, other.max_y, other.min_z),
499
- )
500
- )
501
-
502
- if other.max_z < self.max_z:
503
- # south box
504
- boxes.append(
505
- SelectionBox(
506
- (self.min_x, other.min_y, other.max_z),
507
- (self.max_x, other.max_y, self.max_z),
508
- )
509
- )
510
-
511
- if self.min_x < other.min_x:
512
- # west box
513
- boxes.append(
514
- SelectionBox(
515
- (self.min_x, other.min_y, other.min_z),
516
- (other.min_x, other.max_y, other.max_z),
517
- )
518
- )
519
-
520
- if other.max_x < self.max_x:
521
- # east box
522
- boxes.append(
523
- SelectionBox(
524
- (other.max_x, other.min_y, other.min_z),
525
- (self.max_x, other.max_y, other.max_z),
526
- )
527
- )
528
-
529
- return selection.SelectionGroup(boxes)
530
- else:
531
- # if the boxes do not intersect then the difference is self
532
- return selection.SelectionGroup(self)
533
-
534
- def intersects_vector(
535
- self, origin: PointCoordinatesAny, vector: PointCoordinatesAny
536
- ) -> Optional[float]:
537
- """
538
- Determine if a vector from a given point collides with this selection box.
539
-
540
- :param origin: Location of the origin of the vector
541
- :param vector: The look vector
542
- :return: Multiplier of the vector to the collision location. None if it does not collide
543
- """
544
- # Logic based on https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-box-intersection
545
- for obj in (origin, vector):
546
- if isinstance(obj, (tuple, list)):
547
- if len(obj) != 3 or not all(type(o) in (int, float) for o in obj):
548
- raise ValueError(
549
- "Given tuple/list type must contain three ints or floats."
550
- )
551
- elif isinstance(obj, numpy.ndarray):
552
- if obj.shape != (3,) and numpy.issubdtype(obj.dtype, numpy.number):
553
- raise ValueError(
554
- "Given ndarray type must have a numerical data type with length three."
555
- )
556
- vector = numpy.array(vector)
557
- vector[abs(vector) < 0.000001] = 0.000001
558
- (tmin, tymin, tzmin), (tmax, tymax, tzmax) = numpy.sort(
559
- (self.bounds_array - numpy.array(origin)) / numpy.array(vector), axis=0
560
- )
561
-
562
- if tmin > tymax or tymin > tmax:
563
- return None
564
-
565
- if tymin > tmin:
566
- tmin = tymin
567
-
568
- if tymax < tmax:
569
- tmax = tymax
570
-
571
- if tmin > tzmax or tzmin > tmax:
572
- return None
573
-
574
- if tzmin > tmin:
575
- tmin = tzmin
576
-
577
- if tzmax < tmax:
578
- tmax = tzmax
579
-
580
- if tmin >= 0:
581
- return tmin
582
- elif tmax >= 0:
583
- return tmax
584
- else:
585
- return None
586
-
587
- @staticmethod
588
- def _transform_points(points: numpy.ndarray, matrix: numpy.ndarray):
589
- assert (
590
- isinstance(points, numpy.ndarray)
591
- and len(points.shape) == 2
592
- and points.shape[1] == 3
593
- )
594
- assert isinstance(matrix, numpy.ndarray) and matrix.shape == (4, 4)
595
- points_array = numpy.ones((points.shape[0], 4))
596
- points_array[:, :3] = points
597
- return numpy.matmul(
598
- matrix,
599
- points_array.T,
600
- ).T[:, :3]
601
-
602
- def _iter_transformed_boxes(
603
- self, transform: numpy.ndarray
604
- ) -> Generator[
605
- Tuple[
606
- float, # progress
607
- SelectionBox, # The sub-chunk box.
608
- Union[
609
- numpy.ndarray, # The bool array of which of the transformed blocks are contained.
610
- bool, # If True all blocks are contained, if False no blocks are contained.
611
- ],
612
- Optional[numpy.ndarray], # A float array of where those blocks came from.
613
- ],
614
- None,
615
- None,
616
- ]:
617
- """The core logic for transform and transformed_points"""
618
- assert isinstance(transform, numpy.ndarray) and transform.shape == (4, 4)
619
- inverse_transform = numpy.linalg.inv(transform)
620
- inverse_transform2 = numpy.linalg.inv(
621
- numpy.matmul(displacement_matrix(-0.5, -0.5, -0.5), transform)
622
- )
623
-
624
- def transform_box(box_: SelectionBox, transform_) -> SelectionBox:
625
- """transform a box and get the AABB that contains this rotated box."""
626
-
627
- # find the transformed points of each of the corners
628
- points = numpy.matmul(
629
- transform_,
630
- numpy.array(
631
- list(
632
- itertools.product(
633
- [box_.min_x, box_.max_x],
634
- [box_.min_y, box_.max_y],
635
- [box_.min_z, box_.max_z],
636
- [1],
637
- )
638
- )
639
- ).T,
640
- ).T[:, :3]
641
- # this is a larger AABB that contains the roatated box and a bit more.
642
- return SelectionBox(numpy.min(points, axis=0), numpy.max(points, axis=0))
643
-
644
- aabb = transform_box(self, transform)
645
- count = aabb.sub_chunk_count()
646
- index = 0
647
-
648
- for _, box in aabb.sub_chunk_boxes():
649
- index += 1
650
- original_box = transform_box(box, inverse_transform)
651
- if self.intersects(original_box):
652
- # if the boxes do not intersect then nothing needs doing.
653
- if self.contains_box(original_box):
654
- # if the box is fully contained use the whole box.
655
- yield index / count, box, True, None
656
- else:
657
- # the original points the transformed locations relate to
658
- original_blocks = self._transform_points(
659
- numpy.transpose(
660
- numpy.mgrid[
661
- box.min_x : box.max_x,
662
- box.min_y : box.max_y,
663
- box.min_z : box.max_z,
664
- ],
665
- (1, 2, 3, 0),
666
- ).reshape(-1, 3),
667
- inverse_transform2,
668
- )
669
-
670
- box_shape = box.shape
671
- mask: numpy.ndarray = numpy.all(
672
- numpy.logical_and(
673
- original_blocks < self.max, original_blocks >= self.min
674
- ),
675
- axis=1,
676
- ).reshape(box_shape)
677
-
678
- yield index / count, box, mask, original_blocks.reshape(
679
- box_shape + (3,)
680
- )
681
- else:
682
- yield index / count, box, False, None
683
-
684
- def transformed_points(
685
- self, transform: numpy.ndarray
686
- ) -> Iterable[Tuple[float, Optional[numpy.ndarray], Optional[numpy.ndarray]]]:
687
- """
688
- Get the locations of the transformed blocks and the source blocks they came from.
689
-
690
- :param transform: The matrix that this box will be transformed by.
691
- :return: An iterable of two Nx3 numpy arrays of the source block locations and the destination block locations. The destination locations will be unique but the source may not be and some may not be included.
692
- """
693
- for progress, box, mask, original in self._iter_transformed_boxes(transform):
694
- if isinstance(mask, bool) and mask:
695
- new_points = numpy.transpose(
696
- numpy.mgrid[
697
- box.min_x : box.max_x,
698
- box.min_y : box.max_y,
699
- box.min_z : box.max_z,
700
- ],
701
- (1, 2, 3, 0),
702
- ).reshape(-1, 3)
703
- old_points = self._transform_points(
704
- new_points,
705
- numpy.linalg.inv(
706
- numpy.matmul(displacement_matrix(-0.5, -0.5, -0.5), transform)
707
- ),
708
- )
709
- yield progress, old_points, new_points
710
- elif isinstance(mask, numpy.ndarray) and numpy.any(mask):
711
- yield progress, original[mask], box.min_array + numpy.argwhere(mask)
712
- else:
713
- yield progress, None, None
714
-
715
- def transform(
716
- self, scale: FloatTriplet, rotation: FloatTriplet, translation: FloatTriplet
717
- ) -> selection.SelectionGroup:
718
- """
719
- Creates a :class:`~amulet.api.selection.SelectionGroup` of transformed SelectionBox(es).
720
-
721
- :param scale: A tuple of scaling factors in the x, y and z axis.
722
- :param rotation: The rotation about the x, y and z axis in radians.
723
- :param translation: The translation about the x, y and z axis.
724
- :return: A new :class:`~amulet.api.selection.SelectionGroup` representing the transformed selection.
725
- """
726
- quadrant = math.pi / 2
727
- if all(abs(r - quadrant * round(r / quadrant)) < 0.0001 for r in rotation):
728
- min_point, max_point = numpy.matmul(
729
- transform_matrix(scale, rotation, translation),
730
- numpy.array([[*self.min, 1], [*self.max, 1]]).T,
731
- ).T[:, :3]
732
- return selection.SelectionGroup(SelectionBox(min_point, max_point))
733
- else:
734
- boxes = []
735
- for _, box, mask, _ in self._iter_transformed_boxes(
736
- transform_matrix(scale, rotation, translation)
737
- ):
738
- if isinstance(mask, bool):
739
- if mask:
740
- boxes.append(box)
741
- else:
742
- box_shape = box.shape
743
- any_array: numpy.ndarray = numpy.any(mask, axis=2)
744
- box_2d_shape = numpy.array(any_array.shape)
745
- any_array_flat = any_array.ravel()
746
- start_array = numpy.argmax(mask, axis=2)
747
- stop_array = box_shape[2] - numpy.argmax(
748
- numpy.flip(mask, axis=2), axis=2
749
- )
750
- # effectively a greedy meshing algorithm in 2D
751
- index = 0
752
- while index < any_array_flat.size:
753
- # while there are unhandled true values
754
- index = numpy.argmax(any_array_flat[index:]) + index
755
- # find the first true value
756
- if any_array_flat[index]:
757
- # check that that value is actually True
758
- # create the bounds for the box
759
- min_x, min_y = max_x, max_y = numpy.unravel_index(
760
- index, box_2d_shape
761
- )
762
- # find the z bounds
763
- min_z = start_array[min_x, min_y]
764
- max_z = stop_array[min_x, min_y]
765
- while max_x < box_2d_shape[0] - 1:
766
- # expand in the x while the bounds are the same
767
- new_max_x = max_x + 1
768
- if (
769
- any_array[new_max_x, max_y]
770
- and start_array[new_max_x, max_y] == min_z
771
- and stop_array[new_max_x, max_y] == max_z
772
- ):
773
- # the box z values are the same
774
- max_x = new_max_x
775
- else:
776
- break
777
- while max_y < box_2d_shape[1] - 1:
778
- # expand in the y while the bounds are the same
779
- new_max_y = max_y + 1
780
- if (
781
- numpy.all(any_array[min_x : max_x + 1, new_max_y])
782
- and numpy.all(
783
- start_array[min_x : max_x + 1, new_max_y]
784
- == min_z
785
- )
786
- and numpy.all(
787
- stop_array[min_x : max_x + 1, new_max_y]
788
- == max_z
789
- )
790
- ):
791
- # the box z values are the same
792
- max_y = new_max_y
793
- else:
794
- break
795
- boxes.append(
796
- SelectionBox(
797
- box.min_array + (min_x, min_y, min_z),
798
- box.min_array + (max_x + 1, max_y + 1, max_z),
799
- )
800
- )
801
- any_array[min_x : max_x + 1, min_y : max_y + 1] = False
802
- else:
803
- # If there are no more True values argmax will return 0
804
- break
805
- return selection.SelectionGroup(boxes)
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import numpy
5
+ import math
6
+
7
+ from typing import Tuple, Iterable, Generator, Optional, Union
8
+
9
+ from amulet.api.data_types import (
10
+ BlockCoordinates,
11
+ BlockCoordinatesAny,
12
+ CoordinatesAny,
13
+ ChunkCoordinates,
14
+ SubChunkCoordinates,
15
+ FloatTriplet,
16
+ PointCoordinatesAny,
17
+ )
18
+ from amulet.utils.world_utils import (
19
+ block_coords_to_chunk_coords,
20
+ blocks_slice_to_chunk_slice,
21
+ )
22
+ from amulet.utils.matrix import (
23
+ transform_matrix,
24
+ displacement_matrix,
25
+ )
26
+ from .abstract_selection import AbstractBaseSelection
27
+ from .. import selection
28
+
29
+
30
+ class SelectionBox(AbstractBaseSelection):
31
+ """
32
+ The SelectionBox class represents a single cuboid selection.
33
+
34
+ When combined with :class:`~amulet.api.selection.SelectionGroup` it can represent any arbitrary shape.
35
+ """
36
+
37
+ __slots__ = (
38
+ "_min_x",
39
+ "_min_y",
40
+ "_min_z",
41
+ "_max_x",
42
+ "_max_y",
43
+ "_max_z",
44
+ "_point_1",
45
+ "_point_2",
46
+ )
47
+
48
+ def __init__(self, point_1: BlockCoordinatesAny, point_2: BlockCoordinatesAny):
49
+ """
50
+ Construct a new SelectionBox instance.
51
+
52
+ >>> # a selection box that selects one block.
53
+ >>> box = SelectionBox(
54
+ >>> (0, 0, 0),
55
+ >>> (1, 1, 1)
56
+ >>> )
57
+
58
+ :param point_1: The first point of the selection.
59
+ :param point_2: The second point of the selection.
60
+ """
61
+ box = numpy.array([point_1, point_2]).round().astype(int)
62
+ p1, p2 = box.tolist()
63
+ self._point_1 = tuple(p1)
64
+ self._point_2 = tuple(p2)
65
+ self._min_x, self._min_y, self._min_z = numpy.min(box, 0).tolist()
66
+ self._max_x, self._max_y, self._max_z = numpy.max(box, 0).tolist()
67
+
68
+ @classmethod
69
+ def create_chunk_box(
70
+ cls, cx: int, cz: int, sub_chunk_size: int = 16
71
+ ) -> SelectionBox:
72
+ """
73
+ Get a :class:`SelectionBox` containing the whole of a given chunk.
74
+
75
+ >>> box = SelectionBox.create_chunk_box(1, 2)
76
+ SelectionBox((16, -1073741824, 32), (32, 1073741824, 48))
77
+
78
+ :param cx: The x coordinate of the chunk
79
+ :param cz: The z coordinate of the chunk
80
+ :param sub_chunk_size: The dimension of a sub-chunk. Default 16.
81
+ """
82
+ return cls(
83
+ (cx * sub_chunk_size, -(2**30), cz * sub_chunk_size),
84
+ ((cx + 1) * sub_chunk_size, 2**30, (cz + 1) * sub_chunk_size),
85
+ )
86
+
87
+ @classmethod
88
+ def create_sub_chunk_box(
89
+ cls, cx: int, cy: int, cz: int, sub_chunk_size: int = 16
90
+ ) -> SelectionBox:
91
+ """
92
+ Get a :class:`SelectionBox` containing the whole of a given sub-chunk.
93
+
94
+ >>> SelectionBox.create_sub_chunk_box(1, 0, 2)
95
+ SelectionBox((16, 0, 32), (32, 16, 48))
96
+
97
+ :param cx: The x coordinate of the chunk
98
+ :param cy: The y coordinate of the chunk
99
+ :param cz: The z coordinate of the chunk
100
+ :param sub_chunk_size: The dimension of a sub-chunk. Default 16.
101
+ """
102
+ return cls(
103
+ (cx * sub_chunk_size, cy * sub_chunk_size, cz * sub_chunk_size),
104
+ (
105
+ (cx + 1) * sub_chunk_size,
106
+ (cy + 1) * sub_chunk_size,
107
+ (cz + 1) * sub_chunk_size,
108
+ ),
109
+ )
110
+
111
+ def create_moved_box(
112
+ self, offset: BlockCoordinatesAny, subtract=False
113
+ ) -> SelectionBox:
114
+ """
115
+ Create a new :class:`SelectionBox` based on this one with the coordinates moved by the given offset.
116
+
117
+ :param offset: The amount to move the box.
118
+ :param subtract: If true will subtract the offset rather than adding.
119
+ :return: The new selection with the given offset.
120
+ """
121
+ offset = numpy.array(offset)
122
+ if subtract:
123
+ offset *= -1
124
+ return SelectionBox(offset + self.min, offset + self.max)
125
+
126
+ def chunk_locations(self, sub_chunk_size: int = 16) -> Iterable[ChunkCoordinates]:
127
+ cx_min, cz_min, cx_max, cz_max = block_coords_to_chunk_coords(
128
+ self.min_x,
129
+ self.min_z,
130
+ self.max_x - 1,
131
+ self.max_z - 1,
132
+ sub_chunk_size=sub_chunk_size,
133
+ )
134
+ yield from itertools.product(
135
+ range(cx_min, cx_max + 1), range(cz_min, cz_max + 1)
136
+ )
137
+
138
+ def chunk_boxes(
139
+ self, sub_chunk_size: int = 16
140
+ ) -> Iterable[Tuple[ChunkCoordinates, SelectionBox]]:
141
+ for cx, cz in self.chunk_locations(sub_chunk_size):
142
+ yield (cx, cz), self.intersection(
143
+ SelectionBox.create_chunk_box(cx, cz, sub_chunk_size)
144
+ )
145
+
146
+ def chunk_y_locations(self, sub_chunk_size: int = 16) -> Iterable[int]:
147
+ """
148
+ An iterable of all the sub-chunk y indexes this box intersects.
149
+
150
+ :param sub_chunk_size: The dimension of a sub-chunk. Default 16.
151
+ """
152
+ cy_min, cy_max = block_coords_to_chunk_coords(
153
+ self.min_y, self._max_y - 1, sub_chunk_size=sub_chunk_size
154
+ )
155
+ for cy in range(cy_min, cy_max + 1):
156
+ yield cy
157
+
158
+ def sub_chunk_locations(
159
+ self, sub_chunk_size: int = 16
160
+ ) -> Iterable[SubChunkCoordinates]:
161
+ for cx, cz in self.chunk_locations(sub_chunk_size):
162
+ for cy in self.chunk_y_locations(sub_chunk_size):
163
+ yield cx, cy, cz
164
+
165
+ def chunk_count(self, sub_chunk_size: int = 16) -> int:
166
+ cx_min, cz_min, cx_max, cz_max = block_coords_to_chunk_coords(
167
+ self.min_x,
168
+ self.min_z,
169
+ self.max_x - 1,
170
+ self.max_z - 1,
171
+ sub_chunk_size=sub_chunk_size,
172
+ )
173
+ return (cx_max + 1 - cx_min) * (cz_max + 1 - cz_min)
174
+
175
+ def sub_chunk_count(self, sub_chunk_size: int = 16) -> int:
176
+ cy_min, cy_max = block_coords_to_chunk_coords(
177
+ self.min_y,
178
+ self.max_y - 1,
179
+ sub_chunk_size=sub_chunk_size,
180
+ )
181
+ return (cy_max + 1 - cy_min) * self.chunk_count()
182
+
183
+ def sub_chunk_boxes(
184
+ self, sub_chunk_size: int = 16
185
+ ) -> Iterable[Tuple[SubChunkCoordinates, SelectionBox]]:
186
+ for cx, cy, cz in self.sub_chunk_locations(sub_chunk_size):
187
+ yield (cx, cy, cz), self.intersection(
188
+ SelectionBox.create_sub_chunk_box(cx, cy, cz, sub_chunk_size)
189
+ )
190
+
191
+ def __iter__(self) -> Iterable[BlockCoordinates]:
192
+ """An iterable of all the block locations within this box."""
193
+ return self.blocks
194
+
195
+ @property
196
+ def blocks(self) -> Iterable[BlockCoordinates]:
197
+ return itertools.product(
198
+ range(self._min_x, self._max_x),
199
+ range(self._min_y, self._max_y),
200
+ range(self._min_z, self._max_z),
201
+ )
202
+
203
+ def __repr__(self) -> str:
204
+ return f"SelectionBox({self.point_1}, {self.point_2})"
205
+
206
+ def __str__(self) -> str:
207
+ return f"({self.point_1}, {self.point_2})"
208
+
209
+ def __contains__(self, item: CoordinatesAny) -> bool:
210
+ return self.contains_block(item)
211
+
212
+ def contains_block(self, coords: CoordinatesAny) -> bool:
213
+ return (
214
+ self._min_x <= coords[0] < self._max_x
215
+ and self._min_y <= coords[1] < self._max_y
216
+ and self._min_z <= coords[2] < self._max_z
217
+ )
218
+
219
+ def contains_point(self, coords: CoordinatesAny) -> bool:
220
+ return (
221
+ self._min_x <= coords[0] <= self._max_x
222
+ and self._min_y <= coords[1] <= self._max_y
223
+ and self._min_z <= coords[2] <= self._max_z
224
+ )
225
+
226
+ def __eq__(self, other) -> bool:
227
+ return self.min == other.min and self.max == other.max
228
+
229
+ def __ne__(self, other) -> bool:
230
+ return not self == other
231
+
232
+ def __hash__(self) -> int:
233
+ return hash((*self.min, *self.max))
234
+
235
+ @property
236
+ def slice(self) -> Tuple[slice, slice, slice]:
237
+ """
238
+ Converts the :class:`SelectionBox` minimum/maximum coordinates into slice arguments
239
+
240
+ :return: The :class:`SelectionBox` coordinates as slices in (x,y,z) order
241
+ """
242
+ return (
243
+ slice(self._min_x, self._max_x),
244
+ slice(self._min_y, self._max_y),
245
+ slice(self._min_z, self._max_z),
246
+ )
247
+
248
+ def chunk_slice(
249
+ self, cx: int, cz: int, sub_chunk_size: int = 16
250
+ ) -> Tuple[slice, slice, slice]:
251
+ """
252
+ Get the slice of the box in relative form for a given chunk.
253
+
254
+ >>> SelectionBox((0, 0, 0), (32, 32, 32)).chunk_slice(1, 1)
255
+ (slice(0, 16, None), slice(0, 32, None), slice(0, 16, None))
256
+
257
+ :param cx: The x coordinate of the chunk
258
+ :param cz: The z coordinate of the chunk
259
+ :param sub_chunk_size: The dimension of a sub-chunk. Default 16.
260
+ """
261
+ s_x, s_y, s_z = self.slice
262
+ x_chunk_slice = blocks_slice_to_chunk_slice(s_x, sub_chunk_size, cx)
263
+ z_chunk_slice = blocks_slice_to_chunk_slice(s_z, sub_chunk_size, cz)
264
+ return x_chunk_slice, s_y, z_chunk_slice
265
+
266
+ def sub_chunk_slice(
267
+ self, cx: int, cy: int, cz: int, sub_chunk_size: int = 16
268
+ ) -> Tuple[slice, slice, slice]:
269
+ """
270
+ Get the slice of the box in relative form for a given sub-chunk.
271
+
272
+ >>> SelectionBox((0, 0, 0), (32, 32, 32)).sub_chunk_slice(1, 1, 1)
273
+ (slice(0, 16, None), slice(0, 16, None), slice(0, 16, None))
274
+
275
+ :param cx: The x coordinate of the chunk
276
+ :param cy: The y coordinate of the chunk
277
+ :param cz: The z coordinate of the chunk
278
+ :param sub_chunk_size: The dimension of a sub-chunk. Default 16.
279
+ """
280
+ x_chunk_slice, s_y, z_chunk_slice = self.chunk_slice(cx, cz, sub_chunk_size)
281
+ y_chunk_slice = blocks_slice_to_chunk_slice(s_y, sub_chunk_size, cy)
282
+ return x_chunk_slice, y_chunk_slice, z_chunk_slice
283
+
284
+ @property
285
+ def point_1(self) -> BlockCoordinates:
286
+ """The first value given to the constructor."""
287
+ return self._point_1
288
+
289
+ @property
290
+ def point_2(self) -> BlockCoordinates:
291
+ """The second value given to the constructor."""
292
+ return self._point_2
293
+
294
+ @property
295
+ def points(self) -> Tuple[BlockCoordinates, BlockCoordinates]:
296
+ """The points given to the constructor."""
297
+ return self.point_1, self.point_2
298
+
299
+ @property
300
+ def points_array(self) -> numpy.ndarray:
301
+ """The points given to the constructor as a numpy array."""
302
+ return numpy.array(self.points)
303
+
304
+ @property
305
+ def min_x(self) -> int:
306
+ return self._min_x
307
+
308
+ @property
309
+ def min_y(self) -> int:
310
+ return self._min_y
311
+
312
+ @property
313
+ def min_z(self) -> int:
314
+ return self._min_z
315
+
316
+ @property
317
+ def max_x(self) -> int:
318
+ return self._max_x
319
+
320
+ @property
321
+ def max_y(self) -> int:
322
+ return self._max_y
323
+
324
+ @property
325
+ def max_z(self) -> int:
326
+ return self._max_z
327
+
328
+ @property
329
+ def min(self) -> BlockCoordinates:
330
+ return self._min_x, self._min_y, self._min_z
331
+
332
+ @property
333
+ def min_array(self) -> numpy.ndarray:
334
+ return numpy.array(self.min)
335
+
336
+ @property
337
+ def max(self) -> BlockCoordinates:
338
+ return self._max_x, self._max_y, self._max_z
339
+
340
+ @property
341
+ def max_array(self) -> numpy.ndarray:
342
+ return numpy.array(self.max)
343
+
344
+ @property
345
+ def bounds(self) -> Tuple[BlockCoordinates, BlockCoordinates]:
346
+ return (
347
+ (self._min_x, self._min_y, self._min_z),
348
+ (self._max_x, self._max_y, self._max_z),
349
+ )
350
+
351
+ @property
352
+ def bounds_array(self) -> numpy.ndarray:
353
+ return numpy.array(self.bounds)
354
+
355
+ @property
356
+ def size_x(self) -> int:
357
+ """The length of the box in the x axis."""
358
+ return self._max_x - self._min_x
359
+
360
+ @property
361
+ def size_y(self) -> int:
362
+ """The length of the box in the y axis."""
363
+ return self._max_y - self._min_y
364
+
365
+ @property
366
+ def size_z(self) -> int:
367
+ """The length of the box in the z axis."""
368
+ return self._max_z - self._min_z
369
+
370
+ @property
371
+ def shape(self) -> Tuple[int, int, int]:
372
+ """
373
+ The shape of the box.
374
+
375
+ >>> SelectionBox((0, 0, 0), (1, 1, 1)).shape
376
+ (1, 1, 1)
377
+ """
378
+ return self.size_x, self.size_y, self.size_z
379
+
380
+ @property
381
+ def volume(self) -> int:
382
+ """
383
+ The number of blocks in the box.
384
+
385
+ >>> SelectionBox((0, 0, 0), (1, 1, 1)).shape
386
+ 1
387
+ """
388
+ return self.size_x * self.size_y * self.size_z
389
+
390
+ def touches(self, other: SelectionBox) -> bool:
391
+ """
392
+ Method to check if this instance of :class:`SelectionBox` touches but does not intersect another SelectionBox.
393
+
394
+ :param other: The other SelectionBox
395
+ :return: True if the two :class:`SelectionBox` instances touch, False otherwise
396
+ """
397
+ # It touches if the box does not intersect but intersects when expanded by one block.
398
+ # There may be a simpler way to do this.
399
+ return self.touches_or_intersects(other) and not self.intersects(other)
400
+
401
+ def touches_or_intersects(self, other: SelectionBox) -> bool:
402
+ """
403
+ Method to check if this instance of SelectionBox touches or intersects another SelectionBox.
404
+
405
+ :param other: The other SelectionBox.
406
+ :return: True if the two :class:`SelectionBox` instances touch or intersect, False otherwise.
407
+ """
408
+ return not (
409
+ self.min_x >= other.max_x + 1
410
+ or self.min_y >= other.max_y + 1
411
+ or self.min_z >= other.max_z + 1
412
+ or self.max_x <= other.min_x - 1
413
+ or self.max_y <= other.min_y - 1
414
+ or self.max_z <= other.min_z - 1
415
+ )
416
+
417
+ def intersects(self, other: SelectionBox) -> bool:
418
+ """
419
+ Method to check whether this instance of SelectionBox intersects another SelectionBox.
420
+
421
+ :param other: The other SelectionBox to check for intersection.
422
+ :return: True if the two :class:`SelectionBox` instances intersect, False otherwise.
423
+ """
424
+ return not (
425
+ self.min_x >= other.max_x
426
+ or self.min_y >= other.max_y
427
+ or self.min_z >= other.max_z
428
+ or self.max_x <= other.min_x
429
+ or self.max_y <= other.min_y
430
+ or self.max_z <= other.min_z
431
+ )
432
+
433
+ def contains_box(self, other: SelectionBox) -> bool:
434
+ """
435
+ Method to check if the other SelectionBox other fits entirely within this instance of SelectionBox.
436
+
437
+ :param other: The SelectionBox to test.
438
+ :return: True if other fits with self, False otherwise.
439
+ """
440
+ return (
441
+ self.min_x <= other.min_x
442
+ and self.min_y <= other.min_y
443
+ and self.min_z <= other.min_z
444
+ and other.max_x <= self.max_x
445
+ and other.max_y <= self.max_y
446
+ and other.max_z <= self.max_z
447
+ )
448
+
449
+ def intersection(self, other: SelectionBox) -> SelectionBox:
450
+ return SelectionBox(
451
+ numpy.clip(other.min, self.min, self.max),
452
+ numpy.clip(other.max, self.min, self.max),
453
+ )
454
+
455
+ def subtract(self, other: SelectionBox) -> selection.SelectionGroup:
456
+ """
457
+ Get a :class:`~amulet.api.selection.SelectionGroup` containing boxes that are in self but not in other.
458
+
459
+ This may be empty if other fully contains self or equal to self if they do not intersect.
460
+
461
+ :param other: The SelectionBox to subtract.
462
+ :return:
463
+ """
464
+ if self.intersects(other):
465
+ other = self.intersection(other)
466
+ if self == other:
467
+ # if the two selections are the same there is no difference.
468
+ return selection.SelectionGroup()
469
+ else:
470
+ boxes = []
471
+ if self.min_y < other.min_y:
472
+ # bottom box
473
+ boxes.append(
474
+ SelectionBox(
475
+ (self.min_x, self.min_y, self.min_z),
476
+ (self.max_x, other.min_y, self.max_z),
477
+ )
478
+ )
479
+
480
+ if other.max_y < self.max_y:
481
+ # top box
482
+ boxes.append(
483
+ SelectionBox(
484
+ (self.min_x, other.max_y, self.min_z),
485
+ (self.max_x, self.max_y, self.max_z),
486
+ )
487
+ )
488
+
489
+ # BBB NNN TTT
490
+ # BBB WOE TTT
491
+ # BBB SSS TTT
492
+
493
+ if self.min_z < other.min_z:
494
+ # north box
495
+ boxes.append(
496
+ SelectionBox(
497
+ (self.min_x, other.min_y, self.min_z),
498
+ (self.max_x, other.max_y, other.min_z),
499
+ )
500
+ )
501
+
502
+ if other.max_z < self.max_z:
503
+ # south box
504
+ boxes.append(
505
+ SelectionBox(
506
+ (self.min_x, other.min_y, other.max_z),
507
+ (self.max_x, other.max_y, self.max_z),
508
+ )
509
+ )
510
+
511
+ if self.min_x < other.min_x:
512
+ # west box
513
+ boxes.append(
514
+ SelectionBox(
515
+ (self.min_x, other.min_y, other.min_z),
516
+ (other.min_x, other.max_y, other.max_z),
517
+ )
518
+ )
519
+
520
+ if other.max_x < self.max_x:
521
+ # east box
522
+ boxes.append(
523
+ SelectionBox(
524
+ (other.max_x, other.min_y, other.min_z),
525
+ (self.max_x, other.max_y, other.max_z),
526
+ )
527
+ )
528
+
529
+ return selection.SelectionGroup(boxes)
530
+ else:
531
+ # if the boxes do not intersect then the difference is self
532
+ return selection.SelectionGroup(self)
533
+
534
+ def intersects_vector(
535
+ self, origin: PointCoordinatesAny, vector: PointCoordinatesAny
536
+ ) -> Optional[float]:
537
+ """
538
+ Determine if a vector from a given point collides with this selection box.
539
+
540
+ :param origin: Location of the origin of the vector
541
+ :param vector: The look vector
542
+ :return: Multiplier of the vector to the collision location. None if it does not collide
543
+ """
544
+ # Logic based on https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-box-intersection
545
+ for obj in (origin, vector):
546
+ if isinstance(obj, (tuple, list)):
547
+ if len(obj) != 3 or not all(type(o) in (int, float) for o in obj):
548
+ raise ValueError(
549
+ "Given tuple/list type must contain three ints or floats."
550
+ )
551
+ elif isinstance(obj, numpy.ndarray):
552
+ if obj.shape != (3,) and numpy.issubdtype(obj.dtype, numpy.number):
553
+ raise ValueError(
554
+ "Given ndarray type must have a numerical data type with length three."
555
+ )
556
+ vector = numpy.array(vector)
557
+ vector[abs(vector) < 0.000001] = 0.000001
558
+ (tmin, tymin, tzmin), (tmax, tymax, tzmax) = numpy.sort(
559
+ (self.bounds_array - numpy.array(origin)) / numpy.array(vector), axis=0
560
+ )
561
+
562
+ if tmin > tymax or tymin > tmax:
563
+ return None
564
+
565
+ if tymin > tmin:
566
+ tmin = tymin
567
+
568
+ if tymax < tmax:
569
+ tmax = tymax
570
+
571
+ if tmin > tzmax or tzmin > tmax:
572
+ return None
573
+
574
+ if tzmin > tmin:
575
+ tmin = tzmin
576
+
577
+ if tzmax < tmax:
578
+ tmax = tzmax
579
+
580
+ if tmin >= 0:
581
+ return tmin
582
+ elif tmax >= 0:
583
+ return tmax
584
+ else:
585
+ return None
586
+
587
+ @staticmethod
588
+ def _transform_points(points: numpy.ndarray, matrix: numpy.ndarray):
589
+ assert (
590
+ isinstance(points, numpy.ndarray)
591
+ and len(points.shape) == 2
592
+ and points.shape[1] == 3
593
+ )
594
+ assert isinstance(matrix, numpy.ndarray) and matrix.shape == (4, 4)
595
+ points_array = numpy.ones((points.shape[0], 4))
596
+ points_array[:, :3] = points
597
+ return numpy.matmul(
598
+ matrix,
599
+ points_array.T,
600
+ ).T[:, :3]
601
+
602
+ def _iter_transformed_boxes(
603
+ self, transform: numpy.ndarray
604
+ ) -> Generator[
605
+ Tuple[
606
+ float, # progress
607
+ SelectionBox, # The sub-chunk box.
608
+ Union[
609
+ numpy.ndarray, # The bool array of which of the transformed blocks are contained.
610
+ bool, # If True all blocks are contained, if False no blocks are contained.
611
+ ],
612
+ Optional[numpy.ndarray], # A float array of where those blocks came from.
613
+ ],
614
+ None,
615
+ None,
616
+ ]:
617
+ """The core logic for transform and transformed_points"""
618
+ assert isinstance(transform, numpy.ndarray) and transform.shape == (4, 4)
619
+ inverse_transform = numpy.linalg.inv(transform)
620
+ inverse_transform2 = numpy.linalg.inv(
621
+ numpy.matmul(displacement_matrix(-0.5, -0.5, -0.5), transform)
622
+ )
623
+
624
+ def transform_box(box_: SelectionBox, transform_) -> SelectionBox:
625
+ """transform a box and get the AABB that contains this rotated box."""
626
+
627
+ # find the transformed points of each of the corners
628
+ points = numpy.matmul(
629
+ transform_,
630
+ numpy.array(
631
+ list(
632
+ itertools.product(
633
+ [box_.min_x, box_.max_x],
634
+ [box_.min_y, box_.max_y],
635
+ [box_.min_z, box_.max_z],
636
+ [1],
637
+ )
638
+ )
639
+ ).T,
640
+ ).T[:, :3]
641
+ # this is a larger AABB that contains the roatated box and a bit more.
642
+ return SelectionBox(numpy.min(points, axis=0), numpy.max(points, axis=0))
643
+
644
+ aabb = transform_box(self, transform)
645
+ count = aabb.sub_chunk_count()
646
+ index = 0
647
+
648
+ for _, box in aabb.sub_chunk_boxes():
649
+ index += 1
650
+ original_box = transform_box(box, inverse_transform)
651
+ if self.intersects(original_box):
652
+ # if the boxes do not intersect then nothing needs doing.
653
+ if self.contains_box(original_box):
654
+ # if the box is fully contained use the whole box.
655
+ yield index / count, box, True, None
656
+ else:
657
+ # the original points the transformed locations relate to
658
+ original_blocks = self._transform_points(
659
+ numpy.transpose(
660
+ numpy.mgrid[
661
+ box.min_x : box.max_x,
662
+ box.min_y : box.max_y,
663
+ box.min_z : box.max_z,
664
+ ],
665
+ (1, 2, 3, 0),
666
+ ).reshape(-1, 3),
667
+ inverse_transform2,
668
+ )
669
+
670
+ box_shape = box.shape
671
+ mask: numpy.ndarray = numpy.all(
672
+ numpy.logical_and(
673
+ original_blocks < self.max, original_blocks >= self.min
674
+ ),
675
+ axis=1,
676
+ ).reshape(box_shape)
677
+
678
+ yield index / count, box, mask, original_blocks.reshape(
679
+ box_shape + (3,)
680
+ )
681
+ else:
682
+ yield index / count, box, False, None
683
+
684
+ def transformed_points(
685
+ self, transform: numpy.ndarray
686
+ ) -> Iterable[Tuple[float, Optional[numpy.ndarray], Optional[numpy.ndarray]]]:
687
+ """
688
+ Get the locations of the transformed blocks and the source blocks they came from.
689
+
690
+ :param transform: The matrix that this box will be transformed by.
691
+ :return: An iterable of two Nx3 numpy arrays of the source block locations and the destination block locations. The destination locations will be unique but the source may not be and some may not be included.
692
+ """
693
+ for progress, box, mask, original in self._iter_transformed_boxes(transform):
694
+ if isinstance(mask, bool) and mask:
695
+ new_points = numpy.transpose(
696
+ numpy.mgrid[
697
+ box.min_x : box.max_x,
698
+ box.min_y : box.max_y,
699
+ box.min_z : box.max_z,
700
+ ],
701
+ (1, 2, 3, 0),
702
+ ).reshape(-1, 3)
703
+ old_points = self._transform_points(
704
+ new_points,
705
+ numpy.linalg.inv(
706
+ numpy.matmul(displacement_matrix(-0.5, -0.5, -0.5), transform)
707
+ ),
708
+ )
709
+ yield progress, old_points, new_points
710
+ elif isinstance(mask, numpy.ndarray) and numpy.any(mask):
711
+ yield progress, original[mask], box.min_array + numpy.argwhere(mask)
712
+ else:
713
+ yield progress, None, None
714
+
715
+ def transform(
716
+ self, scale: FloatTriplet, rotation: FloatTriplet, translation: FloatTriplet
717
+ ) -> selection.SelectionGroup:
718
+ """
719
+ Creates a :class:`~amulet.api.selection.SelectionGroup` of transformed SelectionBox(es).
720
+
721
+ :param scale: A tuple of scaling factors in the x, y and z axis.
722
+ :param rotation: The rotation about the x, y and z axis in radians.
723
+ :param translation: The translation about the x, y and z axis.
724
+ :return: A new :class:`~amulet.api.selection.SelectionGroup` representing the transformed selection.
725
+ """
726
+ quadrant = math.pi / 2
727
+ if all(abs(r - quadrant * round(r / quadrant)) < 0.0001 for r in rotation):
728
+ min_point, max_point = numpy.matmul(
729
+ transform_matrix(scale, rotation, translation),
730
+ numpy.array([[*self.min, 1], [*self.max, 1]]).T,
731
+ ).T[:, :3]
732
+ return selection.SelectionGroup(SelectionBox(min_point, max_point))
733
+ else:
734
+ boxes = []
735
+ for _, box, mask, _ in self._iter_transformed_boxes(
736
+ transform_matrix(scale, rotation, translation)
737
+ ):
738
+ if isinstance(mask, bool):
739
+ if mask:
740
+ boxes.append(box)
741
+ else:
742
+ box_shape = box.shape
743
+ any_array: numpy.ndarray = numpy.any(mask, axis=2)
744
+ box_2d_shape = numpy.array(any_array.shape)
745
+ any_array_flat = any_array.ravel()
746
+ start_array = numpy.argmax(mask, axis=2)
747
+ stop_array = box_shape[2] - numpy.argmax(
748
+ numpy.flip(mask, axis=2), axis=2
749
+ )
750
+ # effectively a greedy meshing algorithm in 2D
751
+ index = 0
752
+ while index < any_array_flat.size:
753
+ # while there are unhandled true values
754
+ index = numpy.argmax(any_array_flat[index:]) + index
755
+ # find the first true value
756
+ if any_array_flat[index]:
757
+ # check that that value is actually True
758
+ # create the bounds for the box
759
+ min_x, min_y = max_x, max_y = numpy.unravel_index(
760
+ index, box_2d_shape
761
+ )
762
+ # find the z bounds
763
+ min_z = start_array[min_x, min_y]
764
+ max_z = stop_array[min_x, min_y]
765
+ while max_x < box_2d_shape[0] - 1:
766
+ # expand in the x while the bounds are the same
767
+ new_max_x = max_x + 1
768
+ if (
769
+ any_array[new_max_x, max_y]
770
+ and start_array[new_max_x, max_y] == min_z
771
+ and stop_array[new_max_x, max_y] == max_z
772
+ ):
773
+ # the box z values are the same
774
+ max_x = new_max_x
775
+ else:
776
+ break
777
+ while max_y < box_2d_shape[1] - 1:
778
+ # expand in the y while the bounds are the same
779
+ new_max_y = max_y + 1
780
+ if (
781
+ numpy.all(any_array[min_x : max_x + 1, new_max_y])
782
+ and numpy.all(
783
+ start_array[min_x : max_x + 1, new_max_y]
784
+ == min_z
785
+ )
786
+ and numpy.all(
787
+ stop_array[min_x : max_x + 1, new_max_y]
788
+ == max_z
789
+ )
790
+ ):
791
+ # the box z values are the same
792
+ max_y = new_max_y
793
+ else:
794
+ break
795
+ boxes.append(
796
+ SelectionBox(
797
+ box.min_array + (min_x, min_y, min_z),
798
+ box.min_array + (max_x + 1, max_y + 1, max_z),
799
+ )
800
+ )
801
+ any_array[min_x : max_x + 1, min_y : max_y + 1] = False
802
+ else:
803
+ # If there are no more True values argmax will return 0
804
+ break
805
+ return selection.SelectionGroup(boxes)