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.
- amulet/__init__.py +27 -27
- amulet/__pyinstaller/__init__.py +2 -2
- amulet/__pyinstaller/hook-amulet.py +4 -4
- amulet/_version.py +21 -21
- amulet/api/__init__.py +2 -2
- amulet/api/abstract_base_entity.py +128 -128
- amulet/api/block.py +630 -630
- amulet/api/block_entity.py +71 -71
- amulet/api/cache.py +107 -107
- amulet/api/chunk/__init__.py +6 -6
- amulet/api/chunk/biomes.py +207 -207
- amulet/api/chunk/block_entity_dict.py +175 -175
- amulet/api/chunk/blocks.py +46 -46
- amulet/api/chunk/chunk.py +389 -389
- amulet/api/chunk/entity_list.py +75 -75
- amulet/api/chunk/status.py +167 -167
- amulet/api/data_types/__init__.py +4 -4
- amulet/api/data_types/generic_types.py +4 -4
- amulet/api/data_types/operation_types.py +16 -16
- amulet/api/data_types/world_types.py +49 -49
- amulet/api/data_types/wrapper_types.py +71 -71
- amulet/api/entity.py +74 -74
- amulet/api/errors.py +119 -119
- amulet/api/history/__init__.py +36 -36
- amulet/api/history/base/__init__.py +3 -3
- amulet/api/history/base/base_history.py +26 -26
- amulet/api/history/base/history_manager.py +63 -63
- amulet/api/history/base/revision_manager.py +73 -73
- amulet/api/history/changeable.py +15 -15
- amulet/api/history/data_types.py +7 -7
- amulet/api/history/history_manager/__init__.py +3 -3
- amulet/api/history/history_manager/container.py +102 -102
- amulet/api/history/history_manager/database.py +279 -279
- amulet/api/history/history_manager/meta.py +93 -93
- amulet/api/history/history_manager/object.py +116 -116
- amulet/api/history/revision_manager/__init__.py +2 -2
- amulet/api/history/revision_manager/disk.py +33 -33
- amulet/api/history/revision_manager/ram.py +12 -12
- amulet/api/item.py +75 -75
- amulet/api/level/__init__.py +4 -4
- amulet/api/level/base_level/__init__.py +1 -1
- amulet/api/level/base_level/base_level.py +1035 -1026
- amulet/api/level/base_level/chunk_manager.py +227 -227
- amulet/api/level/base_level/clone.py +389 -389
- amulet/api/level/base_level/player_manager.py +101 -101
- amulet/api/level/immutable_structure/__init__.py +1 -1
- amulet/api/level/immutable_structure/immutable_structure.py +94 -94
- amulet/api/level/immutable_structure/void_format_wrapper.py +117 -117
- amulet/api/level/structure.py +22 -22
- amulet/api/level/world.py +19 -19
- amulet/api/partial_3d_array/__init__.py +2 -2
- amulet/api/partial_3d_array/base_partial_3d_array.py +263 -263
- amulet/api/partial_3d_array/bounded_partial_3d_array.py +528 -528
- amulet/api/partial_3d_array/data_types.py +15 -15
- amulet/api/partial_3d_array/unbounded_partial_3d_array.py +229 -229
- amulet/api/partial_3d_array/util.py +152 -152
- amulet/api/player.py +65 -65
- amulet/api/registry/__init__.py +2 -2
- amulet/api/registry/base_registry.py +34 -34
- amulet/api/registry/biome_manager.py +153 -153
- amulet/api/registry/block_manager.py +156 -156
- amulet/api/selection/__init__.py +2 -2
- amulet/api/selection/abstract_selection.py +315 -315
- amulet/api/selection/box.py +805 -805
- amulet/api/selection/group.py +488 -488
- amulet/api/structure.py +37 -37
- amulet/api/wrapper/__init__.py +8 -8
- amulet/api/wrapper/chunk/interface.py +441 -441
- amulet/api/wrapper/chunk/translator.py +567 -567
- amulet/api/wrapper/format_wrapper.py +772 -772
- amulet/api/wrapper/structure_format_wrapper.py +116 -116
- amulet/api/wrapper/world_format_wrapper.py +63 -63
- amulet/level/__init__.py +1 -1
- amulet/level/formats/anvil_forge_world.py +40 -40
- amulet/level/formats/anvil_world/__init__.py +3 -3
- amulet/level/formats/anvil_world/_sector_manager.py +291 -384
- amulet/level/formats/anvil_world/data_pack/__init__.py +2 -2
- amulet/level/formats/anvil_world/data_pack/data_pack.py +224 -224
- amulet/level/formats/anvil_world/data_pack/data_pack_manager.py +77 -77
- amulet/level/formats/anvil_world/dimension.py +177 -177
- amulet/level/formats/anvil_world/format.py +769 -769
- amulet/level/formats/anvil_world/region.py +384 -384
- amulet/level/formats/construction/__init__.py +3 -3
- amulet/level/formats/construction/format_wrapper.py +515 -515
- amulet/level/formats/construction/interface.py +134 -134
- amulet/level/formats/construction/section.py +60 -60
- amulet/level/formats/construction/util.py +165 -165
- amulet/level/formats/leveldb_world/__init__.py +3 -3
- amulet/level/formats/leveldb_world/chunk.py +33 -33
- amulet/level/formats/leveldb_world/dimension.py +385 -419
- amulet/level/formats/leveldb_world/format.py +659 -641
- amulet/level/formats/leveldb_world/interface/chunk/__init__.py +36 -36
- amulet/level/formats/leveldb_world/interface/chunk/base_leveldb_interface.py +836 -836
- amulet/level/formats/leveldb_world/interface/chunk/generate_interface.py +31 -31
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_0.py +30 -30
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_1.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_10.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_11.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_12.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_13.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_14.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_15.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_16.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_17.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_18.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_19.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_2.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_20.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_21.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_22.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_23.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_24.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_25.py +24 -24
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_26.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_27.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_28.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_29.py +33 -33
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_3.py +57 -57
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_30.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_31.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_32.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_33.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_34.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_35.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_36.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_37.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_38.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_39.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_4.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_40.py +16 -16
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_5.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_6.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_7.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_8.py +180 -180
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_9.py +18 -18
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_chunk_versions.py +79 -79
- amulet/level/formats/mcstructure/__init__.py +3 -3
- amulet/level/formats/mcstructure/chunk.py +50 -50
- amulet/level/formats/mcstructure/format_wrapper.py +408 -408
- amulet/level/formats/mcstructure/interface.py +175 -175
- amulet/level/formats/schematic/__init__.py +3 -3
- amulet/level/formats/schematic/chunk.py +55 -55
- amulet/level/formats/schematic/data_types.py +4 -4
- amulet/level/formats/schematic/format_wrapper.py +373 -373
- amulet/level/formats/schematic/interface.py +142 -142
- amulet/level/formats/sponge_schem/__init__.py +4 -4
- amulet/level/formats/sponge_schem/chunk.py +62 -62
- amulet/level/formats/sponge_schem/format_wrapper.py +463 -463
- amulet/level/formats/sponge_schem/interface.py +118 -118
- amulet/level/formats/sponge_schem/varint/__init__.py +1 -1
- amulet/level/formats/sponge_schem/varint/varint.py +87 -87
- amulet/level/interfaces/chunk/anvil/anvil_0.py +72 -72
- amulet/level/interfaces/chunk/anvil/anvil_1444.py +336 -336
- amulet/level/interfaces/chunk/anvil/anvil_1466.py +94 -94
- amulet/level/interfaces/chunk/anvil/anvil_1467.py +37 -37
- amulet/level/interfaces/chunk/anvil/anvil_1484.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1503.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1519.py +34 -34
- amulet/level/interfaces/chunk/anvil/anvil_1901.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1908.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1912.py +21 -21
- amulet/level/interfaces/chunk/anvil/anvil_1934.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_2203.py +69 -69
- amulet/level/interfaces/chunk/anvil/anvil_2529.py +19 -19
- amulet/level/interfaces/chunk/anvil/anvil_2681.py +76 -76
- amulet/level/interfaces/chunk/anvil/anvil_2709.py +19 -19
- amulet/level/interfaces/chunk/anvil/anvil_2844.py +267 -267
- amulet/level/interfaces/chunk/anvil/anvil_3463.py +19 -19
- amulet/level/interfaces/chunk/anvil/anvil_na.py +607 -607
- amulet/level/interfaces/chunk/anvil/base_anvil_interface.py +326 -326
- amulet/level/load.py +59 -59
- amulet/level/loader.py +95 -95
- amulet/level/translators/chunk/bedrock/__init__.py +267 -267
- amulet/level/translators/chunk/bedrock/bedrock_nbt_blockstate_translator.py +46 -46
- amulet/level/translators/chunk/bedrock/bedrock_numerical_translator.py +39 -39
- amulet/level/translators/chunk/bedrock/bedrock_psudo_numerical_translator.py +37 -37
- amulet/level/translators/chunk/java/java_1_18_translator.py +40 -40
- amulet/level/translators/chunk/java/java_blockstate_translator.py +94 -94
- amulet/level/translators/chunk/java/java_numerical_translator.py +62 -62
- amulet/libs/leveldb/__init__.py +7 -7
- amulet/operations/__init__.py +5 -5
- amulet/operations/clone.py +18 -18
- amulet/operations/delete_chunk.py +32 -32
- amulet/operations/fill.py +30 -30
- amulet/operations/paste.py +65 -65
- amulet/operations/replace.py +58 -58
- amulet/utils/__init__.py +14 -14
- amulet/utils/format_utils.py +41 -41
- amulet/utils/generator.py +15 -15
- amulet/utils/matrix.py +243 -243
- amulet/utils/numpy_helpers.py +46 -46
- amulet/utils/world_utils.py +349 -349
- {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/METADATA +97 -97
- amulet_core-1.9.20.dist-info/RECORD +208 -0
- amulet_core-1.9.19.dist-info/RECORD +0 -208
- {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/WHEEL +0 -0
- {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/entry_points.txt +0 -0
- {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/top_level.txt +0 -0
amulet/api/selection/box.py
CHANGED
|
@@ -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)
|