pylitematic 0.0.3__py3-none-any.whl → 0.0.5__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.
- pylitematic/__init__.py +3 -4
- pylitematic/block_palette.py +129 -0
- pylitematic/block_property.py +13 -13
- pylitematic/block_state.py +34 -12
- pylitematic/geometry.py +67 -103
- pylitematic/property_cache.py +52 -0
- pylitematic/region.py +517 -218
- pylitematic/resource_location.py +6 -10
- pylitematic/schematic.py +24 -13
- pylitematic/test.py +62 -54
- {pylitematic-0.0.3.dist-info → pylitematic-0.0.5.dist-info}/METADATA +4 -1
- pylitematic-0.0.5.dist-info/RECORD +14 -0
- pylitematic-0.0.3.dist-info/RECORD +0 -12
- {pylitematic-0.0.3.dist-info → pylitematic-0.0.5.dist-info}/WHEEL +0 -0
- {pylitematic-0.0.3.dist-info → pylitematic-0.0.5.dist-info}/top_level.txt +0 -0
pylitematic/region.py
CHANGED
@@ -1,18 +1,17 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from abc import ABC, abstractmethod
|
3
4
|
from bitpacking import bitpack, bitunpack
|
4
|
-
|
5
|
+
import copy
|
6
|
+
from itertools import product
|
5
7
|
import nbtlib
|
6
8
|
import numpy as np
|
7
9
|
import twos
|
8
10
|
from typing import Iterator
|
9
11
|
|
10
|
-
from .
|
11
|
-
from .
|
12
|
-
from .
|
13
|
-
|
14
|
-
|
15
|
-
AIR = BlockState("air")
|
12
|
+
from .block_palette import BlockPalette
|
13
|
+
from .block_state import AIR, BlockId, BlockState
|
14
|
+
from .geometry import BlockPosition, Direction, Size3D
|
16
15
|
|
17
16
|
|
18
17
|
class Region:
|
@@ -21,167 +20,126 @@ class Region:
|
|
21
20
|
size: tuple[int, int, int] | Size3D,
|
22
21
|
origin: tuple[int, int, int] | BlockPosition = (0, 0, 0),
|
23
22
|
):
|
24
|
-
|
25
|
-
|
23
|
+
if not isinstance(size, Size3D):
|
24
|
+
size = Size3D(*size)
|
25
|
+
self._size: Size3D = size
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
self.
|
27
|
+
if not isinstance(origin, BlockPosition):
|
28
|
+
origin = BlockPosition(*origin)
|
29
|
+
self._origin: BlockPosition = origin
|
30
30
|
|
31
|
-
|
31
|
+
self._palette: BlockPalette = BlockPalette()
|
32
|
+
self._index_array = np.zeros(abs(self._size), dtype=int)
|
33
|
+
|
34
|
+
# TODO: Add support for (tile) entities and ticks
|
32
35
|
self._entities = nbtlib.List[nbtlib.Compound]()
|
33
36
|
self._tile_entities = nbtlib.List[nbtlib.Compound]()
|
34
37
|
self._block_ticks = nbtlib.List[nbtlib.Compound]()
|
35
38
|
self._fluid_ticks = nbtlib.List[nbtlib.Compound]()
|
36
39
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
index = self._palette_map.get(item)
|
42
|
-
if index is None:
|
43
|
-
return False
|
44
|
-
return np.any(self._blocks == index)
|
45
|
-
elif isinstance(item, BlockId):
|
46
|
-
return any(
|
47
|
-
(bs.id == item and np.any(self._blocks == idx))
|
48
|
-
for bs, idx in self._palette_map.items())
|
49
|
-
else:
|
50
|
-
return False
|
51
|
-
|
52
|
-
def __eq__(self, other) -> bool:
|
53
|
-
palette = np.array(self._palette, dtype=object)
|
54
|
-
|
55
|
-
if isinstance(other, BlockState):
|
56
|
-
matches = np.array([state == other for state in palette])
|
57
|
-
return matches[self._blocks]
|
40
|
+
self._local = LocalRegionView(self)
|
41
|
+
self._world = WorldRegionView(self)
|
42
|
+
self._numpy = NumpyRegionView(self)
|
43
|
+
self._view = self._local
|
58
44
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
else:
|
64
|
-
return NotImplemented
|
65
|
-
|
66
|
-
def __getitem__(self, key):
|
67
|
-
index = self._key_to_index(key)
|
68
|
-
indices = self._blocks[index]
|
69
|
-
if np.isscalar(indices):
|
70
|
-
return self._palette[indices]
|
45
|
+
@property
|
46
|
+
def local(self) -> LocalRegionView:
|
47
|
+
return self._local
|
71
48
|
|
72
|
-
|
49
|
+
@property
|
50
|
+
def world(self) -> WorldRegionView:
|
51
|
+
return self._world
|
73
52
|
|
74
|
-
|
75
|
-
|
53
|
+
@property
|
54
|
+
def numpy(self) -> NumpyRegionView:
|
55
|
+
return self._numpy
|
76
56
|
|
77
|
-
|
78
|
-
|
57
|
+
def set_default_view(self, view: _RegionView) -> None:
|
58
|
+
self._view = view
|
79
59
|
|
80
|
-
|
81
|
-
|
82
|
-
if value not in self._palette_map:
|
83
|
-
self._palette_map[value] = len(self._palette)
|
84
|
-
self._palette.append(value)
|
85
|
-
self._blocks[index] = self._palette_map[value]
|
86
|
-
|
87
|
-
elif isinstance(value, np.ndarray):
|
88
|
-
if value.shape != self._blocks[index].shape:
|
89
|
-
raise ValueError(
|
90
|
-
"Shape mismatch between assigned array and target slice")
|
60
|
+
def __contains__(self, item) -> bool:
|
61
|
+
return item in self._view
|
91
62
|
|
92
|
-
|
93
|
-
|
94
|
-
idx = []
|
95
|
-
for state in unique_states:
|
96
|
-
if state not in self._palette_map:
|
97
|
-
self._palette_map[state] = len(self._palette)
|
98
|
-
self._palette.append(state)
|
99
|
-
idx.append(self._palette_map[state])
|
100
|
-
index_array = np.array(idx, dtype=int)[xdi].reshape(value.shape)
|
101
|
-
self._blocks[index] = index_array
|
102
|
-
else:
|
103
|
-
raise TypeError(
|
104
|
-
"Value must be a BlockState or a list of BlockStates")
|
63
|
+
def __eq__(self, other) -> np.ndarray[bool]:
|
64
|
+
return self._view == other
|
105
65
|
|
106
|
-
def
|
107
|
-
|
108
|
-
index = (index,)
|
109
|
-
ndim = self._blocks.ndim
|
110
|
-
result = []
|
111
|
-
for item in index:
|
112
|
-
if item is Ellipsis:
|
113
|
-
result.extend([slice(None)] * (ndim - len(index) + 1))
|
114
|
-
else:
|
115
|
-
result.append(item)
|
116
|
-
while len(result) < ndim:
|
117
|
-
result.append(slice(None))
|
118
|
-
return tuple(result)
|
119
|
-
|
120
|
-
def _to_internal(self, pos):
|
121
|
-
index = []
|
122
|
-
for i, item in enumerate(pos):
|
123
|
-
offset = self.lower[i]
|
124
|
-
if isinstance(item, int):
|
125
|
-
index.append(item - offset)
|
126
|
-
elif isinstance(item, slice):
|
127
|
-
start = item.start - offset if item.start is not None else None
|
128
|
-
stop = item.stop - offset if item.stop is not None else None
|
129
|
-
index.append(slice(start, stop, item.step))
|
130
|
-
else:
|
131
|
-
index.append(item)
|
132
|
-
return tuple(index)
|
66
|
+
def __ne__(self, other) -> np.ndarray[bool]:
|
67
|
+
return self._view != other
|
133
68
|
|
134
|
-
def
|
135
|
-
return self.
|
69
|
+
def __lt__(self, other) -> np.ndarray[bool]:
|
70
|
+
return self._view < other
|
136
71
|
|
137
|
-
def
|
138
|
-
|
139
|
-
index = tuple(key)
|
140
|
-
else:
|
141
|
-
index = self._expand_index(key)
|
142
|
-
return self._to_internal(index)
|
72
|
+
def __gt__(self, other) -> np.ndarray[bool]:
|
73
|
+
return self._view > other
|
143
74
|
|
144
|
-
def
|
145
|
-
|
146
|
-
# always include minecraft:air in a palette
|
147
|
-
if 0 not in idx:
|
148
|
-
idx = np.insert(idx, 0, 0)
|
149
|
-
index_map = {old: new for new, old in enumerate(idx)}
|
75
|
+
def __getitem__(self, key):
|
76
|
+
return self._view[key]
|
150
77
|
|
151
|
-
|
152
|
-
|
153
|
-
palette_map = {res: idx for idx, res in enumerate(palette)}
|
78
|
+
def __setitem__(self, key, value) -> None:
|
79
|
+
self._view[key] = value
|
154
80
|
|
155
|
-
|
156
|
-
|
157
|
-
lookup[old] = new
|
158
|
-
self._blocks = lookup[self._blocks]
|
81
|
+
def __iter__(self) -> tuple[BlockPosition, BlockState]:
|
82
|
+
return iter(self._view)
|
159
83
|
|
160
|
-
|
161
|
-
self.
|
84
|
+
def clear(self) -> None:
|
85
|
+
self._palette.clear()
|
86
|
+
self._index_array = np.zeros(abs(self._size), dtype=int)
|
162
87
|
|
163
|
-
|
164
|
-
|
88
|
+
self._entities.clear()
|
89
|
+
self._tile_entities.clear()
|
90
|
+
self._block_ticks.clear()
|
91
|
+
self._fluid_ticks.clear()
|
165
92
|
|
93
|
+
def copy(
|
94
|
+
self,
|
95
|
+
origin: tuple[int, int, int] | BlockPosition | None = None,
|
96
|
+
) -> Region:
|
97
|
+
"""Return a copy of the Region."""
|
98
|
+
if origin is None:
|
99
|
+
origin = self._origin
|
100
|
+
|
101
|
+
reg = Region(size=self._size, origin=origin)
|
102
|
+
reg._index_array = copy.deepcopy(self._index_array)
|
103
|
+
reg._palette = self._palette.copy()
|
104
|
+
|
105
|
+
reg._entities = copy.deepcopy(self._entities)
|
106
|
+
reg._tile_entities = copy.deepcopy(self._tile_entities)
|
107
|
+
reg._block_ticks = copy.deepcopy(self._block_ticks)
|
108
|
+
reg._fluid_ticks = copy.deepcopy(self._fluid_ticks)
|
109
|
+
reg._view = type(self._view)(reg)
|
110
|
+
|
111
|
+
return reg
|
112
|
+
|
113
|
+
def flip(self, axis: int | tuple[int] | None = None) -> None:
|
114
|
+
# TODO
|
115
|
+
# * flip size and possibly change origin?
|
116
|
+
# * handle entities
|
117
|
+
# * change BlockState properties accordingly
|
118
|
+
self._index_array = np.flip(self._index_array, axis=axis)
|
119
|
+
|
120
|
+
def reduce_palette(self) -> None:
|
121
|
+
self._index_array = self._palette.reduce(self._index_array)
|
122
|
+
|
123
|
+
# block state en- / decoding (NBT)
|
166
124
|
def _decode_block_states(
|
167
125
|
self,
|
168
126
|
data: nbtlib.LongArray,
|
169
127
|
) -> np.ndarray[int]:
|
170
128
|
states = bitunpack(
|
171
129
|
chunks=[twos.to_unsigned(x, 64) for x in data],
|
172
|
-
field_width=self.
|
130
|
+
field_width=self._palette.bits_per_state,
|
173
131
|
chunk_width=64,
|
174
132
|
)
|
175
|
-
states = list(states)[:self.volume]
|
133
|
+
states = list(states)[:self.volume] # remove trailing bit fields
|
176
134
|
shape = (abs(self.height), abs(self.length), abs(self.width))
|
177
135
|
states = np.asarray(states, dtype=int).reshape(shape) # y,z,x
|
178
136
|
return states.transpose(2, 0, 1) # x,y,z
|
179
137
|
|
180
138
|
def _encode_block_states(self) -> nbtlib.LongArray:
|
181
|
-
states = self.
|
139
|
+
states = self._index_array.transpose(1, 2, 0).ravel() # x,y,z to y,z,x
|
182
140
|
chunks = bitpack(
|
183
141
|
states.tolist(),
|
184
|
-
field_width=self.
|
142
|
+
field_width=self._palette.bits_per_state,
|
185
143
|
chunk_width=64,
|
186
144
|
)
|
187
145
|
return nbtlib.LongArray([twos.to_signed(x, 64) for x in chunks])
|
@@ -190,122 +148,127 @@ class Region:
|
|
190
148
|
def size(self) -> Size3D:
|
191
149
|
return self._size
|
192
150
|
|
193
|
-
@cached_property
|
194
|
-
def sign(self) -> Size3D:
|
195
|
-
return Size3D(*np.sign(self._size))
|
196
|
-
|
197
|
-
@property
|
198
|
-
def origin(self) -> BlockPosition:
|
199
|
-
return self._origin
|
200
|
-
|
201
|
-
@cached_property
|
202
|
-
def limit(self) -> BlockPosition:
|
203
|
-
return self._origin + self._size.end()
|
204
|
-
|
205
|
-
@cached_property
|
206
|
-
def start(self) -> BlockPosition:
|
207
|
-
return BlockPosition(0, 0, 0)
|
208
|
-
|
209
|
-
@cached_property
|
210
|
-
def end(self) -> BlockPosition:
|
211
|
-
return self._size.end()
|
212
|
-
|
213
151
|
@property
|
214
152
|
def width(self) -> int:
|
215
|
-
return self.
|
153
|
+
return self.size.width
|
216
154
|
|
217
155
|
@property
|
218
156
|
def height(self) -> int:
|
219
|
-
return self.
|
157
|
+
return self.size.height
|
220
158
|
|
221
159
|
@property
|
222
160
|
def length(self) -> int:
|
223
|
-
return self.
|
161
|
+
return self.size.length
|
224
162
|
|
225
163
|
@property
|
226
164
|
def volume(self) -> int:
|
227
|
-
return
|
165
|
+
return self.size.volume
|
228
166
|
|
229
167
|
@property
|
230
|
-
def
|
231
|
-
|
232
|
-
return np.count_nonzero(self._blocks)
|
168
|
+
def origin(self) -> BlockPosition:
|
169
|
+
return self._origin
|
233
170
|
|
234
|
-
@
|
235
|
-
def
|
236
|
-
|
171
|
+
@origin.setter
|
172
|
+
def origin(self, value: tuple[int, int, int] | BlockPosition) -> None:
|
173
|
+
if not isinstance(value, BlockPosition):
|
174
|
+
value = BlockPosition(*value)
|
175
|
+
self._origin = value
|
176
|
+
|
177
|
+
def count(self, block: BlockState | BlockId) -> int:
|
178
|
+
return np.sum(self == block).item()
|
237
179
|
|
238
|
-
@
|
180
|
+
@property
|
239
181
|
def lower(self) -> BlockPosition:
|
240
|
-
return
|
182
|
+
return self.local.lower
|
183
|
+
# return self._view.lower
|
241
184
|
|
242
|
-
@
|
185
|
+
@property
|
243
186
|
def upper(self) -> BlockPosition:
|
244
|
-
return
|
187
|
+
return self.local.upper
|
188
|
+
# return self._view.upper
|
245
189
|
|
246
|
-
@
|
190
|
+
@property
|
247
191
|
def bounds(self) -> tuple[BlockPosition, BlockPosition]:
|
248
|
-
return self.
|
192
|
+
return self.local.bounds
|
193
|
+
# return self._view.bounds
|
249
194
|
|
250
|
-
|
251
|
-
|
252
|
-
return BlockPosition(*np.min((self.origin, self.limit), axis=0))
|
195
|
+
def items(self) -> Iterator[tuple[BlockPosition, BlockState]]:
|
196
|
+
return self._view.items()
|
253
197
|
|
254
|
-
|
255
|
-
|
256
|
-
return BlockPosition(*np.max((self.origin, self.limit), axis=0))
|
198
|
+
def positions(self) -> Iterator[BlockPosition]:
|
199
|
+
return self._view.positions()
|
257
200
|
|
258
|
-
|
259
|
-
|
260
|
-
return self.global_lower, self.global_upper
|
201
|
+
def blocks(self) -> Iterator[BlockState]:
|
202
|
+
return self._view.blocks()
|
261
203
|
|
262
|
-
def
|
204
|
+
def where(
|
263
205
|
self,
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
) ->
|
268
|
-
|
269
|
-
include = [include]
|
270
|
-
if isinstance(exclude, BlockState):
|
271
|
-
exclude = [exclude]
|
272
|
-
|
273
|
-
for z, y, x in np.ndindex(self.shape[::-1]):
|
274
|
-
pos = BlockPosition(x, y, z) * self.sign
|
275
|
-
state = self[pos]
|
276
|
-
|
277
|
-
if exclude:
|
278
|
-
if not ignore_props:
|
279
|
-
if state in exclude:
|
280
|
-
continue
|
281
|
-
else:
|
282
|
-
if any(state.id == ex.id for ex in exclude):
|
283
|
-
continue
|
206
|
+
mask: np.ndarray[bool] | BlockState | BlockId,
|
207
|
+
x: BlockState | np.ndarray[BlockState],
|
208
|
+
y: BlockState | np.ndarray[BlockState]| None = None,
|
209
|
+
) -> None:
|
210
|
+
self._view.where(mask, x, y)
|
284
211
|
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
212
|
+
def poswhere(
|
213
|
+
self,
|
214
|
+
mask: np.ndarray[bool] | BlockState | BlockId,
|
215
|
+
) -> list[BlockPosition]:
|
216
|
+
"""Return a list of BlockPositions at which `mask` applies."""
|
217
|
+
return self._view.poswhere(mask)
|
218
|
+
|
219
|
+
# masking relative to BlockState / BlockId
|
220
|
+
def relative_to(
|
221
|
+
self,
|
222
|
+
block: BlockState | BlockId,
|
223
|
+
direction: BlockPosition, # absolute direction
|
224
|
+
) -> np.ndarray[bool]:
|
225
|
+
return self._view.relative_to(block, direction)
|
226
|
+
|
227
|
+
def above_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
228
|
+
return self._view.above_of(block)
|
229
|
+
|
230
|
+
def below_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
231
|
+
return self._view.below_of(block)
|
232
|
+
|
233
|
+
def north_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
234
|
+
return self._view.north_of(block)
|
235
|
+
|
236
|
+
def south_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
237
|
+
return self._view.south_of(block)
|
238
|
+
|
239
|
+
def west_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
240
|
+
return self._view.west_of(block)
|
241
|
+
|
242
|
+
def east_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
243
|
+
return self._view.east_of(block)
|
244
|
+
|
245
|
+
# block position transformations
|
246
|
+
def world_to_local(self, world: BlockPosition) -> BlockPosition:
|
247
|
+
return world - self._origin
|
292
248
|
|
293
|
-
|
249
|
+
def local_to_world(self, local: BlockPosition) -> BlockPosition:
|
250
|
+
return self._origin + local
|
294
251
|
|
295
|
-
def
|
296
|
-
return self.
|
252
|
+
def local_to_numpy(self, local: BlockPosition) -> BlockPosition:
|
253
|
+
return BlockPosition(*self.local.position_to_index(local))
|
297
254
|
|
298
|
-
def
|
299
|
-
return
|
255
|
+
def numpy_to_local(self, index: BlockPosition) -> BlockPosition:
|
256
|
+
return self.local.index_to_position(tuple(index))
|
300
257
|
|
258
|
+
def world_to_numpy(self, world: BlockPosition) -> BlockPosition:
|
259
|
+
return BlockPosition(*self.world.position_to_index(world))
|
260
|
+
|
261
|
+
def numpy_to_world(self, index: BlockPosition) -> BlockPosition:
|
262
|
+
return self.world.index_to_position(tuple(index))
|
263
|
+
|
264
|
+
# NBT conversion
|
301
265
|
def to_nbt(self) -> nbtlib.Compound:
|
302
266
|
nbt = nbtlib.Compound()
|
303
267
|
|
304
268
|
nbt["Position"] = self._origin.to_nbt()
|
305
269
|
nbt["Size"] = self._size.to_nbt()
|
306
270
|
|
307
|
-
|
308
|
-
nbt["BlockStatePalette"] = nbtlib.List[nbtlib.Compound](pal)
|
271
|
+
nbt["BlockStatePalette"] = self._palette.to_nbt()
|
309
272
|
nbt["BlockStates"] = self._encode_block_states()
|
310
273
|
|
311
274
|
nbt["Entities"] = self._entities
|
@@ -315,17 +278,15 @@ class Region:
|
|
315
278
|
|
316
279
|
return nbt
|
317
280
|
|
318
|
-
@
|
319
|
-
def from_nbt(nbt: nbtlib.Compound) -> Region:
|
281
|
+
@classmethod
|
282
|
+
def from_nbt(cls, nbt: nbtlib.Compound) -> Region:
|
320
283
|
pos = BlockPosition.from_nbt(nbt["Position"])
|
321
284
|
size = Size3D.from_nbt(nbt["Size"])
|
322
285
|
|
323
|
-
region =
|
286
|
+
region = cls(origin=pos, size=size)
|
324
287
|
|
325
|
-
region.
|
326
|
-
|
327
|
-
region._palette_map = {bl: i for i, bl in enumerate(region._palette)}
|
328
|
-
region._blocks = region._decode_block_states(nbt["BlockStates"])
|
288
|
+
region._index_array = region._decode_block_states(nbt["BlockStates"])
|
289
|
+
region._palette = BlockPalette.from_nbt(nbt["BlockStatePalette"])
|
329
290
|
|
330
291
|
region._entities = nbt["Entities"]
|
331
292
|
region._tile_entities = nbt["TileEntities"]
|
@@ -333,3 +294,341 @@ class Region:
|
|
333
294
|
region._fluid_ticks = nbt["PendingFluidTicks"]
|
334
295
|
|
335
296
|
return region
|
297
|
+
|
298
|
+
|
299
|
+
class _RegionView(ABC):
|
300
|
+
|
301
|
+
def __init__(self, region: Region) -> None:
|
302
|
+
self.region = region
|
303
|
+
|
304
|
+
@property
|
305
|
+
def _index_array(self) -> np.ndarray[int]:
|
306
|
+
return self.region._index_array
|
307
|
+
|
308
|
+
@property
|
309
|
+
def _palette(self) -> BlockPalette:
|
310
|
+
return self.region._palette
|
311
|
+
|
312
|
+
@abstractmethod
|
313
|
+
def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
|
314
|
+
"""Convert a BlockPosition in the view's coordinate system to the
|
315
|
+
corresponding 3D index in the internal storage array.
|
316
|
+
"""
|
317
|
+
|
318
|
+
@abstractmethod
|
319
|
+
def index_to_position(self, index: tuple[int, int, int]) -> BlockPosition:
|
320
|
+
"""Convert a 3D index in the internal storage array to the corresponding
|
321
|
+
BlockPosition in the view's coordinate system.
|
322
|
+
"""
|
323
|
+
|
324
|
+
@abstractmethod
|
325
|
+
def _align_array(self, arr: np.ndarray) -> np.ndarray:
|
326
|
+
...
|
327
|
+
|
328
|
+
@abstractmethod
|
329
|
+
def _transform_index(self, index):
|
330
|
+
...
|
331
|
+
|
332
|
+
def _state_array(self) -> np.ndarray[BlockState]:
|
333
|
+
return self._palette.get_state(self._index_array)
|
334
|
+
|
335
|
+
def _block_mask(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
336
|
+
if not isinstance(block, (BlockState, BlockId)):
|
337
|
+
raise TypeError(f"'block' needs to be BlockState or BlockId")
|
338
|
+
matches = self._palette == block
|
339
|
+
return matches[self._index_array]
|
340
|
+
|
341
|
+
def __getitem__(self, key):
|
342
|
+
# TODO: allow 'key' to be a BlockState / BlockId
|
343
|
+
if isinstance(key, BlockPosition):
|
344
|
+
# return self.at(key) # TODO
|
345
|
+
key = tuple(key)
|
346
|
+
index = self._transform_index(key)
|
347
|
+
|
348
|
+
indices = self._index_array[index]
|
349
|
+
return self._palette.get_state(indices)
|
350
|
+
|
351
|
+
def __setitem__(self, key, value):
|
352
|
+
if isinstance(key, BlockPosition):
|
353
|
+
# return self.set_at(key, value) # TODO
|
354
|
+
key = tuple(key)
|
355
|
+
index = self._transform_index(key)
|
356
|
+
|
357
|
+
if isinstance(value, list):
|
358
|
+
value = np.array(value, dtype=object)
|
359
|
+
|
360
|
+
if isinstance(value, BlockState):
|
361
|
+
# assign single BlockState to slice
|
362
|
+
if value not in self._palette:
|
363
|
+
self._palette.add_state(value)
|
364
|
+
self._index_array[index] = self._palette.get_index(value)
|
365
|
+
|
366
|
+
elif isinstance(value, np.ndarray):
|
367
|
+
if value.shape != self._index_array[index].shape:
|
368
|
+
# TODO: allow casting
|
369
|
+
raise ValueError(
|
370
|
+
"Shape mismatch between assigned array and target slice")
|
371
|
+
|
372
|
+
# look up (or add) indices for all BlockStates
|
373
|
+
unique_states, xdi = np.unique(value, return_inverse=True)
|
374
|
+
idx = []
|
375
|
+
for state in unique_states:
|
376
|
+
if state not in self._palette:
|
377
|
+
self._palette.add_state(state)
|
378
|
+
idx.append(self._palette.get_index(state))
|
379
|
+
index_array = np.array(idx, dtype=int)[xdi].reshape(value.shape)
|
380
|
+
self._index_array[index] = index_array
|
381
|
+
else:
|
382
|
+
raise TypeError(
|
383
|
+
"Value must be a BlockState or a list of BlockStates")
|
384
|
+
|
385
|
+
def __contains__(self, item) -> bool:
|
386
|
+
if isinstance(item, BlockPosition):
|
387
|
+
return all(self.lower <= item) and all(item <= self.upper)
|
388
|
+
|
389
|
+
elif isinstance(item, BlockState):
|
390
|
+
if not item in self._palette:
|
391
|
+
return False
|
392
|
+
return self._palette.get_index(item) in self._index_array
|
393
|
+
|
394
|
+
elif isinstance(item, BlockId):
|
395
|
+
return any(
|
396
|
+
bs.id == item and idx in self._index_array
|
397
|
+
for bs, idx in self._palette.items())
|
398
|
+
|
399
|
+
else:
|
400
|
+
return False
|
401
|
+
|
402
|
+
def __eq__(self, other) -> np.ndarray[bool]:
|
403
|
+
if not isinstance(other, (BlockState, BlockId)):
|
404
|
+
return NotImplemented
|
405
|
+
mask = self._block_mask(other)
|
406
|
+
return self._align_array(mask)
|
407
|
+
|
408
|
+
def __ne__(self, other) -> np.ndarray[bool]:
|
409
|
+
if not isinstance(other, (BlockState, BlockId)):
|
410
|
+
return NotImplemented
|
411
|
+
return np.invert(self.__eq__(other))
|
412
|
+
|
413
|
+
def __lt__(self, other) -> np.ndarray[bool]:
|
414
|
+
if not isinstance(other, (BlockState, BlockId)):
|
415
|
+
return NotImplemented
|
416
|
+
return self.below_of(other)
|
417
|
+
|
418
|
+
def __gt__(self, other) -> np.ndarray[bool]:
|
419
|
+
if not isinstance(other, (BlockState, BlockId)):
|
420
|
+
return NotImplemented
|
421
|
+
return self.above_of(other)
|
422
|
+
|
423
|
+
def __iter__(self) -> Iterator[tuple[BlockPosition, BlockState]]:
|
424
|
+
return self.items()
|
425
|
+
|
426
|
+
def items(self) -> Iterator[tuple[BlockPosition, BlockState]]:
|
427
|
+
for pos, block in zip(self.positions(), self.blocks()):
|
428
|
+
yield pos, block
|
429
|
+
|
430
|
+
def positions(self) -> Iterator[BlockPosition]:
|
431
|
+
ranges = [
|
432
|
+
range(start, stop, step)
|
433
|
+
for start, stop, step
|
434
|
+
in zip(self.origin, self.origin + self.size, self.size.sign)
|
435
|
+
]
|
436
|
+
for z, y, x in product(*reversed(ranges)):
|
437
|
+
yield BlockPosition(x, y, z)
|
438
|
+
|
439
|
+
def blocks(self) -> Iterator[BlockState]:
|
440
|
+
indices = self._align_array(self._index_array).transpose(2, 1, 0)
|
441
|
+
for block in self._palette.get_state(indices.ravel()):
|
442
|
+
yield block
|
443
|
+
|
444
|
+
def where(
|
445
|
+
self,
|
446
|
+
mask: np.ndarray[bool] | BlockState | BlockId,
|
447
|
+
x: BlockState | np.ndarray[BlockState],
|
448
|
+
y: BlockState | np.ndarray[BlockState] | None = None,
|
449
|
+
) -> None:
|
450
|
+
# TODO: allow 'mask' to be a BlockState / BlockId array
|
451
|
+
# TODO: allow 'x' and 'y' to be Region / _RegionView
|
452
|
+
|
453
|
+
if isinstance(mask, (BlockState | BlockId)):
|
454
|
+
mask = self == mask
|
455
|
+
|
456
|
+
self[mask] = x
|
457
|
+
if y is not None:
|
458
|
+
self[np.invert(mask)] = y
|
459
|
+
|
460
|
+
def poswhere(
|
461
|
+
self,
|
462
|
+
mask: np.ndarray[bool] | BlockState | BlockId,
|
463
|
+
) -> list[BlockPosition]:
|
464
|
+
"""Return a list of BlockPositions at which `mask` applies."""
|
465
|
+
|
466
|
+
if isinstance(mask, (BlockState | BlockId)):
|
467
|
+
mask = self == mask
|
468
|
+
mask = self._align_array(mask)
|
469
|
+
return [self.index_to_position(x) for x in np.argwhere(mask)]
|
470
|
+
|
471
|
+
def _move_mask(
|
472
|
+
self,
|
473
|
+
mask: np.ndarray[bool],
|
474
|
+
direction: BlockPosition,
|
475
|
+
) -> np.ndarray[bool]:
|
476
|
+
result = np.zeros_like(mask, dtype=bool)
|
477
|
+
|
478
|
+
slices_src = [slice(None)] * self._index_array.ndim
|
479
|
+
slices_dst = [slice(None)] * self._index_array.ndim
|
480
|
+
|
481
|
+
for axis, dim in enumerate(direction):
|
482
|
+
if dim == 0:
|
483
|
+
continue
|
484
|
+
elif dim > 0:
|
485
|
+
slices_src[axis] = slice(0, -dim)
|
486
|
+
slices_dst[axis] = slice(dim, None)
|
487
|
+
else:
|
488
|
+
slices_src[axis] = slice(-dim, None)
|
489
|
+
slices_dst[axis] = slice(0, dim)
|
490
|
+
|
491
|
+
result[tuple(slices_dst)] = mask[tuple(slices_src)]
|
492
|
+
return result
|
493
|
+
|
494
|
+
def relative_to(
|
495
|
+
self,
|
496
|
+
block: BlockState | BlockId,
|
497
|
+
direction: BlockPosition,
|
498
|
+
) -> np.ndarray[bool]:
|
499
|
+
mask = self._block_mask(block)
|
500
|
+
mask = self._move_mask(mask, direction)
|
501
|
+
return self._align_array(mask)
|
502
|
+
|
503
|
+
def above_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
504
|
+
return self.relative_to(block, Direction.UP)
|
505
|
+
|
506
|
+
def below_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
507
|
+
return self.relative_to(block, Direction.DOWN)
|
508
|
+
|
509
|
+
def north_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
510
|
+
return self.relative_to(block, Direction.NORTH)
|
511
|
+
|
512
|
+
def south_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
513
|
+
return self.relative_to(block, Direction.SOUTH)
|
514
|
+
|
515
|
+
def west_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
516
|
+
return self.relative_to(block, Direction.WEST)
|
517
|
+
|
518
|
+
def east_of(self, block: BlockState | BlockId) -> np.ndarray[bool]:
|
519
|
+
return self.relative_to(block, Direction.EAST)
|
520
|
+
|
521
|
+
property
|
522
|
+
@abstractmethod
|
523
|
+
def origin(self) -> BlockPosition:
|
524
|
+
...
|
525
|
+
|
526
|
+
@property
|
527
|
+
@abstractmethod
|
528
|
+
def size(self) -> Size3D:
|
529
|
+
...
|
530
|
+
|
531
|
+
@property
|
532
|
+
def limit(self) -> BlockPosition:
|
533
|
+
return self.origin + self.size.limit
|
534
|
+
|
535
|
+
@property
|
536
|
+
def lower(self) -> BlockPosition:
|
537
|
+
return BlockPosition(*np.min((self.origin, self.limit), axis=0))
|
538
|
+
|
539
|
+
@property
|
540
|
+
def upper(self) -> BlockPosition:
|
541
|
+
return BlockPosition(*np.max((self.origin, self.limit), axis=0))
|
542
|
+
|
543
|
+
@property
|
544
|
+
def bounds(self) -> tuple[BlockPosition, BlockPosition]:
|
545
|
+
return self.lower, self.upper
|
546
|
+
|
547
|
+
|
548
|
+
class NumpyRegionView(_RegionView):
|
549
|
+
|
550
|
+
@property
|
551
|
+
def origin(self) -> BlockPosition:
|
552
|
+
return BlockPosition(0, 0, 0)
|
553
|
+
# reg_size = self.region._size
|
554
|
+
# return BlockPosition(*np.where(reg_size, 0, -(reg_size + 1)))
|
555
|
+
|
556
|
+
@property
|
557
|
+
def size(self) -> Size3D:
|
558
|
+
return abs(self.region._size)
|
559
|
+
# return self.region._size
|
560
|
+
|
561
|
+
def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
|
562
|
+
return tuple(pos)
|
563
|
+
|
564
|
+
def index_to_position(self, index: tuple[int, int, int]) -> BlockPosition:
|
565
|
+
return BlockPosition(*index)
|
566
|
+
|
567
|
+
def _align_array(self, arr: np.ndarray) -> np.ndarray:
|
568
|
+
return arr
|
569
|
+
|
570
|
+
def _transform_index(self, index):
|
571
|
+
return index
|
572
|
+
|
573
|
+
|
574
|
+
class _OrientedView(_RegionView):
|
575
|
+
|
576
|
+
@property
|
577
|
+
def size(self) -> Size3D:
|
578
|
+
return self.region._size
|
579
|
+
|
580
|
+
@property
|
581
|
+
def negative_axes(self) -> tuple[int,...]:
|
582
|
+
return tuple(np.argwhere(self.size < 0).flatten().tolist())
|
583
|
+
|
584
|
+
def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
|
585
|
+
return pos - self.lower
|
586
|
+
|
587
|
+
def index_to_position(self, index: tuple[int, int, int]) -> BlockPosition:
|
588
|
+
return self.lower + index
|
589
|
+
|
590
|
+
def _align_array(self, arr: np.ndarray) -> np.ndarray:
|
591
|
+
return np.flip(arr, axis=self.negative_axes)
|
592
|
+
|
593
|
+
def _transform_index(self, key):
|
594
|
+
if isinstance(key, (int, np.integer, slice, type(Ellipsis))):
|
595
|
+
key = (key,)
|
596
|
+
|
597
|
+
if isinstance(key, tuple):
|
598
|
+
key = list(key)
|
599
|
+
for i, k in enumerate(key):
|
600
|
+
offset = self.lower[i]
|
601
|
+
if isinstance(k, (int, np.integer)):
|
602
|
+
key[i] = k - offset
|
603
|
+
elif isinstance(k, slice):
|
604
|
+
start = k.start - offset if k.start is not None else None
|
605
|
+
stop = k.stop - offset if k.stop is not None else None
|
606
|
+
key[i] = slice(start, stop, k.step)
|
607
|
+
else:
|
608
|
+
# Ellipsis
|
609
|
+
key[i] = k
|
610
|
+
return tuple(key)
|
611
|
+
|
612
|
+
elif isinstance(key, np.ndarray) and key.dtype == bool:
|
613
|
+
# boolean indexing
|
614
|
+
key = self._align_array(key)
|
615
|
+
if key.shape != self._index_array.shape:
|
616
|
+
raise IndexError("Boolean index must match region shape.")
|
617
|
+
return key
|
618
|
+
|
619
|
+
else:
|
620
|
+
return key
|
621
|
+
|
622
|
+
|
623
|
+
class LocalRegionView(_OrientedView):
|
624
|
+
|
625
|
+
@property
|
626
|
+
def origin(self) -> BlockPosition:
|
627
|
+
return BlockPosition(0, 0, 0)
|
628
|
+
|
629
|
+
|
630
|
+
class WorldRegionView(_OrientedView):
|
631
|
+
|
632
|
+
@property
|
633
|
+
def origin(self) -> BlockPosition:
|
634
|
+
return self.region._origin
|