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