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/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
- from functools import cached_property
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 .block_state import BlockState
11
- from .geometry import BlockPosition, Size3D
12
- from .resource_location import BlockId
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
- self._origin: BlockPosition = BlockPosition(*origin)
25
- self._size: Size3D = Size3D(*size)
23
+ if not isinstance(size, Size3D):
24
+ size = Size3D(*size)
25
+ self._size: Size3D = size
26
26
 
27
- self._palette: list[BlockState] = [AIR]
28
- self._palette_map: dict[BlockState, int] = {AIR: 0}
29
- self._blocks = np.zeros(abs(self._size), dtype=int)
27
+ if not isinstance(origin, BlockPosition):
28
+ origin = BlockPosition(*origin)
29
+ self._origin: BlockPosition = origin
30
30
 
31
- # TODO: Add support
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
- def __contains__(self, item) -> bool:
38
- if isinstance(item, BlockPosition):
39
- return all(self.lower <= item) and all(item <= self.upper)
40
- elif isinstance(item, BlockState):
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
- elif isinstance(other, BlockId):
60
- matches = np.array([state.id == other for state in palette])
61
- return matches[self._blocks]
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
- return np.array(self._palette, dtype=object)[indices]
49
+ @property
50
+ def world(self) -> WorldRegionView:
51
+ return self._world
73
52
 
74
- def __setitem__(self, key, value) -> None:
75
- index = self._key_to_index(key)
53
+ @property
54
+ def numpy(self) -> NumpyRegionView:
55
+ return self._numpy
76
56
 
77
- if isinstance(value, list):
78
- value = np.array(value, dtype=object)
57
+ def set_default_view(self, view: _RegionView) -> None:
58
+ self._view = view
79
59
 
80
- if isinstance(value, BlockState):
81
- # assign single BlockState to slice
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
- # look up (or add) indices for all BlockStates
93
- unique_states, xdi = np.unique(value, return_inverse=True)
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 _expand_index(self, index):
107
- if not isinstance(index, tuple):
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 _from_internal(self, index: tuple[int, int, int]) -> BlockPosition:
135
- return self.lower + index
69
+ def __lt__(self, other) -> np.ndarray[bool]:
70
+ return self._view < other
136
71
 
137
- def _key_to_index(self, key):
138
- if isinstance(key, BlockPosition):
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 compact_palette(self) -> None:
145
- idx = np.unique(self._blocks)
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
- # compacted palette and mapping
152
- palette = np.array(self._palette, dtype=object)[idx].tolist()
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
- lookup = np.full(max(index_map) + 1, -1, dtype=int)
156
- for old, new in index_map.items():
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
- self._palette = palette
161
- self._palette_map = palette_map
84
+ def clear(self) -> None:
85
+ self._palette.clear()
86
+ self._index_array = np.zeros(abs(self._size), dtype=int)
162
87
 
163
- def _bits_per_state(self) -> int:
164
- return max(2, (len(self._palette) - 1).bit_length())
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._bits_per_state(),
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._blocks.transpose(1, 2, 0).ravel() # x,y,z to y,z,x
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._bits_per_state(),
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._size.width
153
+ return self.size.width
216
154
 
217
155
  @property
218
156
  def height(self) -> int:
219
- return self._size.height
157
+ return self.size.height
220
158
 
221
159
  @property
222
160
  def length(self) -> int:
223
- return self._size.length
161
+ return self.size.length
224
162
 
225
163
  @property
226
164
  def volume(self) -> int:
227
- return np.prod(self.shape).item()
165
+ return self.size.volume
228
166
 
229
167
  @property
230
- def block_count(self) -> int:
231
- # TODO: Add filter BlockState and rename to count()
232
- return np.count_nonzero(self._blocks)
168
+ def origin(self) -> BlockPosition:
169
+ return self._origin
233
170
 
234
- @property
235
- def shape(self) -> tuple[int, int, int]:
236
- return self._blocks.shape
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
- @cached_property
180
+ @property
239
181
  def lower(self) -> BlockPosition:
240
- return BlockPosition(*np.min((self.start, self.end), axis=0))
182
+ return self.local.lower
183
+ # return self._view.lower
241
184
 
242
- @cached_property
185
+ @property
243
186
  def upper(self) -> BlockPosition:
244
- return BlockPosition(*np.max((self.start, self.end), axis=0))
187
+ return self.local.upper
188
+ # return self._view.upper
245
189
 
246
- @cached_property
190
+ @property
247
191
  def bounds(self) -> tuple[BlockPosition, BlockPosition]:
248
- return self.lower, self.upper
192
+ return self.local.bounds
193
+ # return self._view.bounds
249
194
 
250
- @cached_property
251
- def global_lower(self) -> BlockPosition:
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
- @cached_property
255
- def global_upper(self) -> BlockPosition:
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
- @cached_property
259
- def global_bounds(self) -> tuple[BlockPosition, BlockPosition]:
260
- return self.global_lower, self.global_upper
201
+ def blocks(self) -> Iterator[BlockState]:
202
+ return self._view.blocks()
261
203
 
262
- def blocks(
204
+ def where(
263
205
  self,
264
- include: BlockState | list[BlockState] | None = None,
265
- exclude: BlockState | list[BlockState] | None = None,
266
- ignore_props: bool = False,
267
- ) -> Iterator[tuple[BlockPosition, BlockState]]:
268
- if isinstance(include, BlockState):
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
- if include:
286
- if not ignore_props:
287
- if state not in include:
288
- continue
289
- else:
290
- if not any(state.id == s.id for s in include):
291
- continue
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
- yield pos, state
249
+ def local_to_world(self, local: BlockPosition) -> BlockPosition:
250
+ return self._origin + local
294
251
 
295
- def to_global(self, local_pos: BlockPosition) -> BlockPosition:
296
- return self._origin + local_pos
252
+ def local_to_numpy(self, local: BlockPosition) -> BlockPosition:
253
+ return BlockPosition(*self.local.position_to_index(local))
297
254
 
298
- def to_local(self, global_pos: BlockPosition) -> BlockPosition:
299
- return global_pos - self._origin
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
- pal = [block.to_nbt() for block in self._palette]
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
- @staticmethod
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 = Region(origin=pos, size=size)
286
+ region = cls(origin=pos, size=size)
324
287
 
325
- region._palette = [
326
- BlockState.from_nbt(block) for block in nbt["BlockStatePalette"]]
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