pylitematic 0.0.4__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 CHANGED
@@ -1,7 +1,6 @@
1
- __version__ = "0.0.4"
1
+ __version__ = "0.0.5"
2
2
 
3
- from .block_state import BlockState
3
+ from .block_state import AIR, BlockId, BlockState
4
4
  from .geometry import BlockPosition, Direction, Size3D
5
5
  from .region import Region
6
- from .resource_location import BlockId
7
6
  from .schematic import Schematic
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import nbtlib
5
+ import numpy as np
6
+ from typing import Iterator
7
+
8
+ from .block_state import AIR, BlockId, BlockState
9
+
10
+
11
+ class BlockPalette:
12
+ def __init__(self):
13
+ self._states: list[BlockState] = []
14
+ self._map: dict[BlockState, int] = {}
15
+ self.add_state(AIR)
16
+
17
+ def __len__(self):
18
+ return len(self._states)
19
+
20
+ def __str__(self) -> str:
21
+ return str({str(s): i for s, i in self._map.items()})
22
+
23
+ def __repr__(self) -> str:
24
+ return f"{type(self).__name__}({self._map})"
25
+
26
+ def __contains__(self, key) -> bool:
27
+ # TODO: add BlockId comparison?
28
+ if not isinstance(key, BlockState):
29
+ return NotImplemented
30
+ return key in self._map
31
+
32
+ def __eq__(self, other) -> bool | np.ndarray[bool]:
33
+ if not isinstance(other, (BlockState, BlockId)):
34
+ return NotImplemented
35
+ return np.array(self) == other
36
+
37
+ def __iter__(self) -> Iterator[tuple[BlockState, int]]:
38
+ return self.items()
39
+
40
+ def __array__(self, dtype: type | None = None, copy: bool = True):
41
+ arr = np.array(self._states, dtype=object)
42
+ return arr.copy() if copy else arr
43
+
44
+ def states(self) -> Iterator[BlockState]:
45
+ for state in self._map.keys():
46
+ yield state
47
+
48
+ def indices(self) -> Iterator[int]:
49
+ for index in self._map.values():
50
+ yield index
51
+
52
+ def items(self) -> Iterator[tuple[BlockState, int]]:
53
+ for state, index in self._map.items():
54
+ yield state, index
55
+
56
+ @property
57
+ def bits_per_state(self) -> int:
58
+ return max(2, (len(self) - 1).bit_length())
59
+
60
+ def copy(self) -> BlockPalette:
61
+ pal = BlockPalette()
62
+ pal._states = copy.deepcopy(self._states)
63
+ pal._map = copy.deepcopy(self._map)
64
+ return pal
65
+
66
+ def clear(self) -> None:
67
+ self._states.clear()
68
+ self._map.clear()
69
+ self.add_state(AIR)
70
+
71
+ def add_state(self, state: BlockState) -> int:
72
+ if state in self:
73
+ return self._map[state]
74
+ else:
75
+ index = len(self)
76
+ self._states.append(state)
77
+ self._map[state] = index
78
+ return index
79
+
80
+ def get_state(
81
+ self,
82
+ index: int | np.ndarray[int],
83
+ ) -> BlockState | np.ndarray[BlockState]:
84
+ return np.array(self._states, dtype=object)[np.array(index, dtype=int)]
85
+
86
+ def get_index(
87
+ self,
88
+ state: BlockState | np.ndarray[BlockState],
89
+ add_missing: bool = False,
90
+ ) -> int | np.ndarray[int]:
91
+ state = np.asarray(state, dtype=object)
92
+ unique_states, xdi = np.unique(state, return_inverse=True)
93
+ idx = []
94
+ for block in unique_states:
95
+ if block not in self and not add_missing:
96
+ raise KeyError(f"BlockState '{block!s}' not found in palette.")
97
+ idx.append(self.add_state(block))
98
+
99
+ index = np.array(idx, dtype=int)[xdi].reshape(state.shape)
100
+ return index.item() if np.isscalar(index) else index
101
+
102
+ def reduce(self, indices: np.ndarray[int]) -> None:
103
+ if not (isinstance(indices, np.ndarray) and indices.dtype == int):
104
+ raise TypeError("'indices' has to be a numpy array of integers")
105
+
106
+ unique_idx = np.unique(indices)
107
+ if 0 not in unique_idx:
108
+ # always include minecraft:air as the first entry in the palette
109
+ unique_idx = np.insert(unique_idx, 0, 0)
110
+ self._states = np.array(self._states, dtype=object)[unique_idx].tolist()
111
+ self._map = {state: i for i, state in enumerate(self._states)}
112
+
113
+ old_new_map = {old: new for new, old in enumerate(unique_idx)}
114
+ lookup = np.full(max(old_new_map) + 1, -1, dtype=int)
115
+ for old, new in old_new_map.items():
116
+ lookup[old] = new
117
+ return lookup[indices]
118
+
119
+ def to_nbt(self) -> nbtlib.List[nbtlib.Compound]:
120
+ pal = [state.to_nbt() for state in self._states]
121
+ return nbtlib.List[nbtlib.Compound](pal)
122
+
123
+ @classmethod
124
+ def from_nbt(cls, nbt: nbtlib.List[nbtlib.Compound]) -> BlockPalette:
125
+ states = [BlockState.from_nbt(block) for block in nbt]
126
+ pal = cls()
127
+ pal._states = states
128
+ pal._map = {state: i for i, state in enumerate(states)}
129
+ return pal
@@ -1,11 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from copy import deepcopy
4
+ from dataclasses import dataclass
4
5
  from nbtlib import Compound
5
6
  from typing import Any, Iterator
6
7
 
7
- from .resource_location import BlockId
8
8
  from .block_property import Properties
9
+ from .resource_location import ResourceLocation
10
+
11
+
12
+ @dataclass(frozen=True, order=True)
13
+ class BlockId(ResourceLocation):
14
+
15
+ def __eq__(self, other: Any) -> bool:
16
+ if isinstance(other, BlockState):
17
+ return self == other.id
18
+ else:
19
+ return super().__eq__(other)
9
20
 
10
21
 
11
22
  class BlockState:
@@ -42,10 +53,18 @@ class BlockState:
42
53
 
43
54
  def __eq__(self, other: Any) -> bool:
44
55
  if isinstance(other, str):
45
- other = BlockState.from_string(other)
46
- elif not isinstance(other, BlockState):
56
+ try:
57
+ other = BlockState.from_string(other)
58
+ return self == other
59
+ except ValueError:
60
+ return False
61
+
62
+ if isinstance(other, BlockId):
63
+ return self.id == other
64
+ elif isinstance(other, BlockState):
65
+ return (self.id, self._props) == (other.id, other._props)
66
+ else:
47
67
  return NotImplemented
48
- return (self.id, self._props) == (other.id, other._props)
49
68
 
50
69
  def __lt__(self, other: Any) -> bool:
51
70
  if not isinstance(other, BlockState):
@@ -117,3 +136,6 @@ class BlockState:
117
136
 
118
137
  def without_props(self) -> BlockState:
119
138
  return BlockState(self.id)
139
+
140
+
141
+ AIR = BlockState("air")
pylitematic/geometry.py CHANGED
@@ -51,7 +51,7 @@ class Vec3i:
51
51
  return self.__add__(other)
52
52
 
53
53
  def __sub__(self, other) -> Vec3i:
54
- return self.__add__(-other)
54
+ return type(self)(*(np.array(self) - other))
55
55
 
56
56
  def __rsub__(self, other) -> Vec3i:
57
57
  return -self.__sub__(other)
@@ -0,0 +1,52 @@
1
+ import functools
2
+
3
+
4
+ class cached_property:
5
+ def __init__(self, func):
6
+ self.func = func
7
+ self.name = func.__name__
8
+ functools.update_wrapper(self, func)
9
+
10
+ def __get__(self, instance, owner=None):
11
+ if instance is None:
12
+ return self
13
+
14
+ if not hasattr(instance, "_cache"):
15
+ raise AttributeError("Instance must have a '_cache' attribute")
16
+
17
+ cache = instance._cache
18
+ if self.name not in cache:
19
+ cache[self.name] = self.func(instance)
20
+ return cache[self.name]
21
+
22
+
23
+ def clears_cache(func):
24
+ @functools.wraps(func)
25
+ def wrapper(self, *args, **kwargs):
26
+ if hasattr(self, "_cache"):
27
+ self._cache.clear()
28
+ return func(self, *args, **kwargs)
29
+ return wrapper
30
+
31
+
32
+ class PropertyCache(dict):
33
+ def __getattr__(self, name):
34
+ try:
35
+ return self[name]
36
+ except KeyError:
37
+ raise AttributeError(f"No cached value for {name!r}")
38
+
39
+ def __setattr__(self, name, value):
40
+ self[name] = value
41
+
42
+ def __delattr__(self, name):
43
+ try:
44
+ del self[name]
45
+ except KeyError:
46
+ raise AttributeError(f"No cached value named {name!r}")
47
+
48
+
49
+ class PropertyCacheMixin:
50
+ def __init__(self, *args, **kwargs) -> None:
51
+ super().__init__(*args, **kwargs)
52
+ self._cache = PropertyCache()
pylitematic/region.py CHANGED
@@ -2,19 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from bitpacking import bitpack, bitunpack
5
- from functools import cached_property
5
+ import copy
6
6
  from itertools import product
7
7
  import nbtlib
8
8
  import numpy as np
9
9
  import twos
10
10
  from typing import Iterator
11
11
 
12
- from .block_state import BlockState
13
- from .geometry import BlockPosition, Size3D
14
- from .resource_location import BlockId
15
-
16
-
17
- AIR = BlockState("air")
12
+ from .block_palette import BlockPalette
13
+ from .block_state import AIR, BlockId, BlockState
14
+ from .geometry import BlockPosition, Direction, Size3D
18
15
 
19
16
 
20
17
  class Region:
@@ -31,9 +28,8 @@ class Region:
31
28
  origin = BlockPosition(*origin)
32
29
  self._origin: BlockPosition = origin
33
30
 
34
- self._palette: list[BlockState] = [AIR] # TODO: add clear method
35
- self._palette_map: dict[BlockState, int] = {AIR: 0} # TODO: bind tighter to _palette
36
- self._blocks = np.zeros(abs(self._size), dtype=int)
31
+ self._palette: BlockPalette = BlockPalette()
32
+ self._index_array = np.zeros(abs(self._size), dtype=int)
37
33
 
38
34
  # TODO: Add support for (tile) entities and ticks
39
35
  self._entities = nbtlib.List[nbtlib.Compound]()
@@ -44,6 +40,7 @@ class Region:
44
40
  self._local = LocalRegionView(self)
45
41
  self._world = WorldRegionView(self)
46
42
  self._numpy = NumpyRegionView(self)
43
+ self._view = self._local
47
44
 
48
45
  @property
49
46
  def local(self) -> LocalRegionView:
@@ -57,67 +54,92 @@ class Region:
57
54
  def numpy(self) -> NumpyRegionView:
58
55
  return self._numpy
59
56
 
57
+ def set_default_view(self, view: _RegionView) -> None:
58
+ self._view = view
59
+
60
60
  def __contains__(self, item) -> bool:
61
- return item in self.local
61
+ return item in self._view
62
+
63
+ def __eq__(self, other) -> np.ndarray[bool]:
64
+ return self._view == other
65
+
66
+ def __ne__(self, other) -> np.ndarray[bool]:
67
+ return self._view != other
62
68
 
63
- def __eq__(self, other) -> bool:
64
- return self.local == other
69
+ def __lt__(self, other) -> np.ndarray[bool]:
70
+ return self._view < other
65
71
 
66
- def __ne__(self, other) -> bool:
67
- return self.local != other
72
+ def __gt__(self, other) -> np.ndarray[bool]:
73
+ return self._view > other
68
74
 
69
75
  def __getitem__(self, key):
70
- return self.local[key]
76
+ return self._view[key]
71
77
 
72
78
  def __setitem__(self, key, value) -> None:
73
- self.local[key] = value
79
+ self._view[key] = value
74
80
 
75
81
  def __iter__(self) -> tuple[BlockPosition, BlockState]:
76
- return iter(self.local)
77
-
78
- def compact_palette(self) -> None:
79
- # TODO: determine all appropriate places to call this method
80
- idx = np.unique(self._blocks)
81
- if 0 not in idx:
82
- # always include minecraft:air as the first entry in the palette
83
- idx = np.insert(idx, 0, 0)
84
- index_map = {old: new for new, old in enumerate(idx)}
82
+ return iter(self._view)
85
83
 
86
- # compacted palette and mapping
87
- palette = np.array(self._palette, dtype=object)[idx].tolist()
88
- palette_map = {res: idx for idx, res in enumerate(palette)}
84
+ def clear(self) -> None:
85
+ self._palette.clear()
86
+ self._index_array = np.zeros(abs(self._size), dtype=int)
89
87
 
90
- lookup = np.full(max(index_map) + 1, -1, dtype=int)
91
- for old, new in index_map.items():
92
- lookup[old] = new
93
- self._blocks = lookup[self._blocks]
88
+ self._entities.clear()
89
+ self._tile_entities.clear()
90
+ self._block_ticks.clear()
91
+ self._fluid_ticks.clear()
94
92
 
95
- self._palette = palette
96
- self._palette_map = palette_map
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)
97
122
 
98
123
  # block state en- / decoding (NBT)
99
- def _bits_per_state(self) -> int:
100
- return max(2, (len(self._palette) - 1).bit_length())
101
-
102
124
  def _decode_block_states(
103
125
  self,
104
126
  data: nbtlib.LongArray,
105
127
  ) -> np.ndarray[int]:
106
128
  states = bitunpack(
107
129
  chunks=[twos.to_unsigned(x, 64) for x in data],
108
- field_width=self._bits_per_state(),
130
+ field_width=self._palette.bits_per_state,
109
131
  chunk_width=64,
110
132
  )
111
- states = list(states)[:self.volume]
133
+ states = list(states)[:self.volume] # remove trailing bit fields
112
134
  shape = (abs(self.height), abs(self.length), abs(self.width))
113
135
  states = np.asarray(states, dtype=int).reshape(shape) # y,z,x
114
136
  return states.transpose(2, 0, 1) # x,y,z
115
137
 
116
138
  def _encode_block_states(self) -> nbtlib.LongArray:
117
- 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
118
140
  chunks = bitpack(
119
141
  states.tolist(),
120
- field_width=self._bits_per_state(),
142
+ field_width=self._palette.bits_per_state,
121
143
  chunk_width=64,
122
144
  )
123
145
  return nbtlib.LongArray([twos.to_signed(x, 64) for x in chunks])
@@ -146,31 +168,79 @@ class Region:
146
168
  def origin(self) -> BlockPosition:
147
169
  return self._origin
148
170
 
149
- @property
150
- def block_count(self) -> int:
151
- # TODO: Add filter BlockStates / BlockIds and rename to count()
152
- return np.sum(self != AIR).item()
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()
153
179
 
154
180
  @property
155
181
  def lower(self) -> BlockPosition:
156
182
  return self.local.lower
183
+ # return self._view.lower
157
184
 
158
185
  @property
159
186
  def upper(self) -> BlockPosition:
160
187
  return self.local.upper
188
+ # return self._view.upper
161
189
 
162
190
  @property
163
191
  def bounds(self) -> tuple[BlockPosition, BlockPosition]:
164
192
  return self.local.bounds
193
+ # return self._view.bounds
165
194
 
166
195
  def items(self) -> Iterator[tuple[BlockPosition, BlockState]]:
167
- return self.local.items()
196
+ return self._view.items()
168
197
 
169
198
  def positions(self) -> Iterator[BlockPosition]:
170
- return self.local.positions()
199
+ return self._view.positions()
171
200
 
172
201
  def blocks(self) -> Iterator[BlockState]:
173
- return self.local.blocks()
202
+ return self._view.blocks()
203
+
204
+ def where(
205
+ self,
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)
211
+
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)
174
244
 
175
245
  # block position transformations
176
246
  def world_to_local(self, world: BlockPosition) -> BlockPosition:
@@ -198,8 +268,7 @@ class Region:
198
268
  nbt["Position"] = self._origin.to_nbt()
199
269
  nbt["Size"] = self._size.to_nbt()
200
270
 
201
- pal = [block.to_nbt() for block in self._palette]
202
- nbt["BlockStatePalette"] = nbtlib.List[nbtlib.Compound](pal)
271
+ nbt["BlockStatePalette"] = self._palette.to_nbt()
203
272
  nbt["BlockStates"] = self._encode_block_states()
204
273
 
205
274
  nbt["Entities"] = self._entities
@@ -216,10 +285,8 @@ class Region:
216
285
 
217
286
  region = cls(origin=pos, size=size)
218
287
 
219
- region._palette = [
220
- BlockState.from_nbt(block) for block in nbt["BlockStatePalette"]]
221
- region._palette_map = {bl: i for i, bl in enumerate(region._palette)}
222
- 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"])
223
290
 
224
291
  region._entities = nbt["Entities"]
225
292
  region._tile_entities = nbt["TileEntities"]
@@ -233,27 +300,26 @@ class _RegionView(ABC):
233
300
 
234
301
  def __init__(self, region: Region) -> None:
235
302
  self.region = region
236
- # TODO: add support for (tile) entities and ticks
237
303
 
238
304
  @property
239
- def _blocks(self) -> np.ndarray[int]:
240
- return self.region._blocks
305
+ def _index_array(self) -> np.ndarray[int]:
306
+ return self.region._index_array
241
307
 
242
308
  @property
243
- def _palette(self) -> list[BlockState]:
309
+ def _palette(self) -> BlockPalette:
244
310
  return self.region._palette
245
311
 
246
- @property
247
- def _palette_map(self) -> dict[BlockState, int]:
248
- return self.region._palette_map
249
-
250
312
  @abstractmethod
251
313
  def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
252
- ...
314
+ """Convert a BlockPosition in the view's coordinate system to the
315
+ corresponding 3D index in the internal storage array.
316
+ """
253
317
 
254
318
  @abstractmethod
255
319
  def index_to_position(self, index: tuple[int, int, int]) -> BlockPosition:
256
- ...
320
+ """Convert a 3D index in the internal storage array to the corresponding
321
+ BlockPosition in the view's coordinate system.
322
+ """
257
323
 
258
324
  @abstractmethod
259
325
  def _align_array(self, arr: np.ndarray) -> np.ndarray:
@@ -263,17 +329,24 @@ class _RegionView(ABC):
263
329
  def _transform_index(self, index):
264
330
  ...
265
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
+
266
341
  def __getitem__(self, key):
342
+ # TODO: allow 'key' to be a BlockState / BlockId
267
343
  if isinstance(key, BlockPosition):
268
344
  # return self.at(key) # TODO
269
345
  key = tuple(key)
270
346
  index = self._transform_index(key)
271
347
 
272
- indices = self._blocks[index]
273
- if np.isscalar(indices):
274
- return self._palette[indices]
275
- else:
276
- return np.array(self._palette, dtype=object)[indices]
348
+ indices = self._index_array[index]
349
+ return self._palette.get_state(indices)
277
350
 
278
351
  def __setitem__(self, key, value):
279
352
  if isinstance(key, BlockPosition):
@@ -286,13 +359,12 @@ class _RegionView(ABC):
286
359
 
287
360
  if isinstance(value, BlockState):
288
361
  # assign single BlockState to slice
289
- if value not in self._palette_map:
290
- self._palette_map[value] = len(self._palette)
291
- self._palette.append(value)
292
- self._blocks[index] = self._palette_map[value]
362
+ if value not in self._palette:
363
+ self._palette.add_state(value)
364
+ self._index_array[index] = self._palette.get_index(value)
293
365
 
294
366
  elif isinstance(value, np.ndarray):
295
- if value.shape != self._blocks[index].shape:
367
+ if value.shape != self._index_array[index].shape:
296
368
  # TODO: allow casting
297
369
  raise ValueError(
298
370
  "Shape mismatch between assigned array and target slice")
@@ -301,12 +373,11 @@ class _RegionView(ABC):
301
373
  unique_states, xdi = np.unique(value, return_inverse=True)
302
374
  idx = []
303
375
  for state in unique_states:
304
- if state not in self._palette_map:
305
- self._palette_map[state] = len(self._palette)
306
- self._palette.append(state)
307
- idx.append(self._palette_map[state])
376
+ if state not in self._palette:
377
+ self._palette.add_state(state)
378
+ idx.append(self._palette.get_index(state))
308
379
  index_array = np.array(idx, dtype=int)[xdi].reshape(value.shape)
309
- self._blocks[index] = index_array
380
+ self._index_array[index] = index_array
310
381
  else:
311
382
  raise TypeError(
312
383
  "Value must be a BlockState or a list of BlockStates")
@@ -316,21 +387,39 @@ class _RegionView(ABC):
316
387
  return all(self.lower <= item) and all(item <= self.upper)
317
388
 
318
389
  elif isinstance(item, BlockState):
319
- index = self._palette_map.get(item)
320
- if index is None:
390
+ if not item in self._palette:
321
391
  return False
322
- return index in self._blocks
323
- # return np.any(self._blocks == index).item()
392
+ return self._palette.get_index(item) in self._index_array
324
393
 
325
394
  elif isinstance(item, BlockId):
326
395
  return any(
327
- # bs.id == item and np.any(self._blocks == idx)
328
- bs.id == item and idx in self._blocks
329
- for bs, idx in self._palette_map.items())
396
+ bs.id == item and idx in self._index_array
397
+ for bs, idx in self._palette.items())
330
398
 
331
399
  else:
332
400
  return False
333
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
+
334
423
  def __iter__(self) -> Iterator[tuple[BlockPosition, BlockState]]:
335
424
  return self.items()
336
425
 
@@ -348,29 +437,86 @@ class _RegionView(ABC):
348
437
  yield BlockPosition(x, y, z)
349
438
 
350
439
  def blocks(self) -> Iterator[BlockState]:
351
- indices = self._align_array(self._blocks).transpose(2, 1, 0).ravel()
352
- palette = np.array(self._palette, dtype=object)
353
- for block in palette[indices]:
440
+ indices = self._align_array(self._index_array).transpose(2, 1, 0)
441
+ for block in self._palette.get_state(indices.ravel()):
354
442
  yield block
355
443
 
356
- def __eq__(self, other) -> np.ndarray[bool]:
357
- palette = np.array(self._palette, dtype=object)
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
358
452
 
359
- if isinstance(other, BlockState):
360
- matches = np.array([state == other for state in palette])
361
- mask = matches[self._blocks]
453
+ if isinstance(mask, (BlockState | BlockId)):
454
+ mask = self == mask
362
455
 
363
- elif isinstance(other, BlockId):
364
- matches = np.array([state.id == other for state in palette])
365
- mask = matches[self._blocks]
456
+ self[mask] = x
457
+ if y is not None:
458
+ self[np.invert(mask)] = y
366
459
 
367
- else:
368
- return NotImplemented
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)]
369
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)
370
501
  return self._align_array(mask)
371
502
 
372
- def __ne__(self, other) -> np.ndarray[bool]:
373
- return np.invert(self.__eq__(other))
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)
374
520
 
375
521
  property
376
522
  @abstractmethod
@@ -382,15 +528,15 @@ class _RegionView(ABC):
382
528
  def size(self) -> Size3D:
383
529
  ...
384
530
 
385
- @cached_property
531
+ @property
386
532
  def limit(self) -> BlockPosition:
387
533
  return self.origin + self.size.limit
388
534
 
389
- @cached_property
535
+ @property
390
536
  def lower(self) -> BlockPosition:
391
537
  return BlockPosition(*np.min((self.origin, self.limit), axis=0))
392
538
 
393
- @cached_property
539
+ @property
394
540
  def upper(self) -> BlockPosition:
395
541
  return BlockPosition(*np.max((self.origin, self.limit), axis=0))
396
542
 
@@ -404,10 +550,13 @@ class NumpyRegionView(_RegionView):
404
550
  @property
405
551
  def origin(self) -> BlockPosition:
406
552
  return BlockPosition(0, 0, 0)
553
+ # reg_size = self.region._size
554
+ # return BlockPosition(*np.where(reg_size, 0, -(reg_size + 1)))
407
555
 
408
556
  @property
409
557
  def size(self) -> Size3D:
410
558
  return abs(self.region._size)
559
+ # return self.region._size
411
560
 
412
561
  def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
413
562
  return tuple(pos)
@@ -428,7 +577,7 @@ class _OrientedView(_RegionView):
428
577
  def size(self) -> Size3D:
429
578
  return self.region._size
430
579
 
431
- @cached_property
580
+ @property
432
581
  def negative_axes(self) -> tuple[int,...]:
433
582
  return tuple(np.argwhere(self.size < 0).flatten().tolist())
434
583
 
@@ -463,7 +612,7 @@ class _OrientedView(_RegionView):
463
612
  elif isinstance(key, np.ndarray) and key.dtype == bool:
464
613
  # boolean indexing
465
614
  key = self._align_array(key)
466
- if key.shape != self._blocks.shape:
615
+ if key.shape != self._index_array.shape:
467
616
  raise IndexError("Boolean index must match region shape.")
468
617
  return key
469
618
 
@@ -67,7 +67,3 @@ class ResourceLocation:
67
67
  @classmethod
68
68
  def from_nbt(cls, nbt: nbtlib.String) -> ResourceLocation:
69
69
  return cls.from_string(str(nbt))
70
-
71
-
72
- class BlockId(ResourceLocation):
73
- ...
pylitematic/schematic.py CHANGED
@@ -9,7 +9,8 @@ import twos
9
9
  from typing import Iterator
10
10
 
11
11
  from pylitematic.geometry import BlockPosition, Size3D
12
- from pylitematic.region import Region
12
+ from pylitematic.region import AIR, Region
13
+ from pylitematic.block_state import AIR
13
14
 
14
15
 
15
16
  DEFAULT_VERSION_MAJOR: int = 7
@@ -75,8 +76,6 @@ class Schematic:
75
76
  self.version_minor = version_minor
76
77
  self.mc_version = mc_version
77
78
 
78
- self.modified: bool = True
79
-
80
79
  def __getitem__(self, key):
81
80
  return self._regions[key]
82
81
 
@@ -123,7 +122,8 @@ class Schematic:
123
122
 
124
123
  @property
125
124
  def blocks(self) -> int:
126
- return sum(reg.block_count for reg in self._regions.values())
125
+ return sum(
126
+ reg.volume - reg.count(AIR) for reg in self._regions.values())
127
127
 
128
128
  @property
129
129
  def region_count(self) -> int:
@@ -158,7 +158,11 @@ class Schematic:
158
158
  def modified_at(self) -> datetime:
159
159
  return datetime.fromtimestamp(int(self._modified_at / 1000))
160
160
 
161
+ def clear(self) -> None:
162
+ self._regions = {}
163
+
161
164
  def save(self, path: pathlib.Path | str) -> None:
165
+ self._modified_at = int(time.time() * 1000)
162
166
  file = nbtlib.File(self.to_nbt())
163
167
  file.save(path, gzipped=True, byteorder="big")
164
168
 
@@ -170,6 +174,10 @@ class Schematic:
170
174
  return cls.from_nbt(nbt)
171
175
 
172
176
  def to_nbt(self) -> nbtlib.Compound:
177
+ if not self.region_count:
178
+ raise ValueError(
179
+ f"Schematic {self.name!r} needs at least one region")
180
+
173
181
  nbt = nbtlib.Compound()
174
182
 
175
183
  # meta data
@@ -193,7 +201,7 @@ class Schematic:
193
201
  # regions
194
202
  regions = nbtlib.Compound()
195
203
  for name, region in self.regions():
196
- region.compact_palette()
204
+ region.reduce_palette()
197
205
  regions[name] = region.to_nbt()
198
206
  nbt["Regions"] = regions
199
207
 
pylitematic/test.py CHANGED
@@ -1,72 +1,79 @@
1
- import numpy as np
2
- import pathlib
3
- from pylitematic import BlockPosition, BlockId, BlockState, Region, Schematic, Size3D
1
+ from pylitematic import (
2
+ BlockPosition,
3
+ BlockId,
4
+ BlockState,
5
+ Region,
6
+ Schematic,
7
+ Size3D,
8
+ )
4
9
 
5
- # path = pathlib.Path("/mnt/d/minecraft/schematics/Litematica/test/subs.litematic")
6
- # path = pathlib.Path("/mnt/d/minecraft/schematics/Litematica/test/regions.litematic")
7
- # path = pathlib.Path("/mnt/d/minecraft/schematics/Litematica/test/creeper_test.litematic")
8
- # stone = BlockState.from_string("minecraft:stone")
9
- # dirt = BlockState.from_string("minecraft:dirt")
10
- # s = Schematic.load(path)
11
- # print(f"{s.volume=} {s.size=} {s.bounds=}")
12
- # for name, reg in s.regions():
13
- # print(name)
14
- # print(f"\t{reg.shape=} {reg.volume=} {reg.block_count=}")
15
- # print(f"\t{reg.origin=!s} {reg.limit=!s}")
16
- # print(f"\t{reg.start=!s} {reg.end=!s}")
17
- # print(f"\t{reg.lower=!s} {reg.upper=!s} {reg.size=}")
18
- # # print(f"\t{reg[..., 1, 0]}")
19
- # # print(f"\t{reg[:][1][0]}")
20
- # # print(f"\t{reg[BlockPosition(0, 1, 0)]}")
21
- # # reg[1,1,1] = BlockState.from_string("minecraft:stone")
22
- # # print("lol: ", reg[reg.end])
23
- # # reg[0,:,0] = BlockState("minecraft:obsidian")
24
- # # reg[0,:,0] = [dirt, stone, dirt]
25
- # # print(reg[...,0])
26
- # # print(reg[np.array([BlockPosition(0, 0, 0), BlockPosition(1, 1, 1)])])
27
- # # print(f"\t{reg[:]}")
28
- # # for pos, state in reg.blocks(exclude_air=True):
29
- # # print(pos, state)
30
- # # for pos, state in reg.blocks((BlockState("oak_log", axis="x"), BlockState("spruce_log", axis="z")), ignore_props=True):
31
- # # reg[...,-1] = stone
32
- # for pos, state in reg.blocks(exclude=BlockState("air")):
33
- # print(f"\t{pos} {reg._to_internal(pos)}: {state}")
34
- # for pos, state in reg.blocks(include=BlockState("lime_wool")):
35
- # reg[pos] = BlockState("minecraft:blue_wool")
36
- # for pos, state in reg.blocks(include=BlockState("tripwire"), ignore_props=True):
37
- # reg[pos] = BlockState("minecraft:glass")
38
- # # print(BlockState("oak_log", axis="x") in reg)
39
- # # print(BlockPosition(1, 1, 0) in reg)
40
- # # print(ResourceLocation("birch_log") in reg)
41
- # # print(reg[0,:,2])
42
- # s.save("/mnt/d/minecraft/schematics/Litematica/test/aaa.litematic")
43
10
 
44
11
  air = BlockState("air")
45
12
  stone = BlockState("stone")
46
13
  dirt = BlockState("dirt")
47
14
  grass = BlockState("grass_block")
15
+ water = BlockState("water")
16
+ lava = BlockState("lava")
17
+ sand = BlockState("sand")
18
+
48
19
  cobble = BlockState("cobblestone")
49
20
  mossy_cobble = BlockState("mossy_cobblestone")
21
+
50
22
  snow = BlockState("snow_block")
23
+ ice = BlockState("ice")
51
24
  pumpkin = BlockState("carved_pumpkin", facing="west")
25
+ jack = BlockState("jack_o_lantern", facing="west")
52
26
 
53
- ground = Region(size=Size3D(16, 9, 16), origin=BlockPosition(0, 0, 0))
54
- ground.local[:,:5,:] = stone
55
- ground.local[:,5:8,:] = dirt
56
- ground.local[:,8:,:] = grass
27
+ ground = Region(size=Size3D(16, 9, 16))
28
+ ground.local[:,:5,:] = stone # 4 stone layers
29
+ ground.local[:,5:8,:] = dirt # 3 dirt layers
30
+ ground[(ground > dirt) & (ground == BlockId("air"))] = grass # grass above exposed dirt
31
+ ground.numpy[2:5,-1,2:5] = water # small pond
32
+ ground[ground.relative_to(water, BlockPosition(4, 0, 0))] = lava # lava pool
57
33
 
58
34
  boulder = Region(size=(4, 4, 4), origin=ground.origin+[6, ground.height, 6])
59
- boulder[:] = mossy_cobble
60
- boulder.numpy[:,-2:,:] = cobble
35
+ boulder[:] = mossy_cobble # fill with mossy cobblestone
36
+ boulder.numpy[:,-2:,:] = cobble # upper two layers of cobblestone
37
+
38
+ snow_man = Region(
39
+ size=(1, -3, 1), origin=boulder.origin+[2, boulder.upper.y+3, 1])
40
+ snow_man.set_default_view(snow_man.numpy)
41
+ snow_man[...] = snow # fill with snow
42
+ snow_man[0,-1,0] = pumpkin # pumpkin on top
43
+
44
+ snow_woman = snow_man.copy(origin=snow_man.origin+[-1, 0, 1])
45
+ # snow_woman[snow_woman == snow] = ice # replace snow with ice
46
+ snow_woman.where(snow, ice, jack) # replace snow with ice and rest with lanterns
61
47
 
62
- # snow_man = Region(size=(1, 3, 1), origin=boulder.origin+[1, boulder.height, 1])
63
- snow_man = Region(size=(1, -3, 1), origin=boulder.origin+[1, boulder.upper.y+3, 1])
64
- snow_man[...] = snow
65
- # snow_man.numpy[:,:2,:] = [[[snow], [snow]]]
66
- snow_man[0,snow_man.upper.y,0] = pumpkin
48
+ clones = []
49
+ start = BlockPosition(1, ground.upper.y, ground.upper.z-1)
50
+ for i in range(5):
51
+ clones.append(
52
+ snow_man.copy(origin=start+(i * 3, 3, -i)))
67
53
 
68
- schem = Schematic(name="a_scene", author="Boscawinks", description="A simple scene")
54
+ cuboid = Region(origin=(14, ground.upper.y+3, 4), size=(-2,-3,-4))
55
+ cuboid[:,-2:0,:] = sand
56
+ cuboid.where((cuboid < air) & (cuboid == sand), stone)
57
+ for pos, block in cuboid.numpy.items():
58
+ print(f"{pos}:\t{block}")
59
+
60
+ schem = Schematic(
61
+ name="a_scene", author="Boscawinks", description="A simple scene")
69
62
  schem.add_region("ground", ground)
70
63
  schem.add_region("boulder", boulder)
71
64
  schem.add_region("snow_man", snow_man)
72
- schem.save(f"/mnt/d/minecraft/schematics/Litematica/test/{schem.name}.litematic")
65
+ schem.add_region("snow_woman", snow_woman)
66
+ for i, clone in enumerate(clones):
67
+ schem.add_region(f"clone_{i+1}", clone)
68
+ schem.add_region("cuboid", cuboid)
69
+ schem.save(
70
+ f"/mnt/d/minecraft/schematics/Litematica/test/{schem.name}.litematic")
71
+
72
+ # print(boulder[...,-1])
73
+
74
+ # from pathlib import Path
75
+ # path = Path("/mnt/d/minecraft/schematics/Litematica/turtle/turtle_8x8.litematic")
76
+ # turtle = Schematic.load(path)
77
+ # for name, reg in turtle.regions():
78
+ # reg[reg != BlockState("air")] = BlockState("blue_wool")
79
+ # turtle.save(path.with_suffix(".blue.litematic"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylitematic
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Load, modify, and save Litematica schematics
5
5
  Author-email: Boscawinks <bosca.winks@gmx.de>
6
6
  License: GPL-3.0-only
@@ -0,0 +1,14 @@
1
+ pylitematic/__init__.py,sha256=KlmeOfpqQ64bSbOTEj2IcLwARfwzg3kTbUtBpRAgpU0,188
2
+ pylitematic/block_palette.py,sha256=9k1rzLgmoUK2jkuKo4v0nuE37wVFvDF3jja59NqYY_Y,4280
3
+ pylitematic/block_property.py,sha256=HGxDSngEr-tcwuZPGn6ohHQQ65OL4MaTP9ytDAvuSCw,7714
4
+ pylitematic/block_state.py,sha256=4CQ5V6dbXuGY3vRp2wXJCqPIV9xIHZbuzJ7JgY7oLA4,4064
5
+ pylitematic/geometry.py,sha256=gPsvjnp6tSpQ1uA9hbsKO4EdjdMuxV2wm6KwxFaedSQ,4211
6
+ pylitematic/property_cache.py,sha256=BFY-fh6xDBpJTZ2QBndeJKYVAObVwMLA4TYfXiiCBcA,1358
7
+ pylitematic/region.py,sha256=_08u-lbR3MxBQIngRZq-72lS9X73ynUR1Vpu0J_jtN4,20468
8
+ pylitematic/resource_location.py,sha256=VgTPayrTc0xJNM6smG9vseajFGVccL2wwtLKUyG9qYo,2125
9
+ pylitematic/schematic.py,sha256=Nm7gV6e0ayYSJK8lhcjnm5_c5KGC-MSRTzpTbqfvKcA,8367
10
+ pylitematic/test.py,sha256=9OQzQONGjs6TqyinwdSk6o9yysXe2EKSiOeHM45hGIM,2663
11
+ pylitematic-0.0.5.dist-info/METADATA,sha256=dOGSClod9zp_e8LFQiNHDKM8lzZNE7QIxNZCXyoaT6I,1159
12
+ pylitematic-0.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ pylitematic-0.0.5.dist-info/top_level.txt,sha256=sYUxm6O7Dh5TzuP-kPFe2FHJWUuwHFO69vN2VBiEG4A,12
14
+ pylitematic-0.0.5.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- pylitematic/__init__.py,sha256=Cu9tl3jcE6c1VPcMFkrNq5ddWhGZFd7OT_lhItHBdsI,213
2
- pylitematic/block_property.py,sha256=HGxDSngEr-tcwuZPGn6ohHQQ65OL4MaTP9ytDAvuSCw,7714
3
- pylitematic/block_state.py,sha256=PuwE3b7zf8Gw-wV3DOXYYwfvLrUJmkj0H_XC7yGMKdw,3540
4
- pylitematic/geometry.py,sha256=x_rjJ0gO7uhUJqEg9es8nzD-p3rCiSM9vpdwF7JukAM,4194
5
- pylitematic/region.py,sha256=iMX95qkd-OAWP8GUyi-_DBYgtphy8djgLb5lRR_Ssu8,15150
6
- pylitematic/resource_location.py,sha256=bqw3Oh9Bx-oWZdm-Qj_FoqpNOh3gc6DPvKqCm6DIDSo,2168
7
- pylitematic/schematic.py,sha256=nKH5EjU2ZvuJzjyO84kmKrcKI0jqmNfzq1UXNIroKYQ,8090
8
- pylitematic/test.py,sha256=weexc1mKqC7f2Uayd3KuXLsXBOvMCmLGqZrekyeVEiI,3284
9
- pylitematic-0.0.4.dist-info/METADATA,sha256=pKIVzFpc22EELK8IyPmlM2UB5999l_SGozXuZucRRDQ,1159
10
- pylitematic-0.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- pylitematic-0.0.4.dist-info/top_level.txt,sha256=sYUxm6O7Dh5TzuP-kPFe2FHJWUuwHFO69vN2VBiEG4A,12
12
- pylitematic-0.0.4.dist-info/RECORD,,