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