pylitematic 0.0.3__py3-none-any.whl → 0.0.4__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,7 @@
1
- __version__ = "0.0.3"
1
+ __version__ = "0.0.4"
2
2
 
3
3
  from .block_state import BlockState
4
- from .geometry import BlockPosition, Size3D
4
+ from .geometry import BlockPosition, Direction, Size3D
5
5
  from .region import Region
6
6
  from .resource_location import BlockId
7
7
  from .schematic import Schematic
@@ -94,10 +94,10 @@ class Properties(dict):
94
94
  def to_string(self) -> str:
95
95
  return str(self)
96
96
 
97
- @staticmethod
98
- def from_string(string: str) -> Properties:
97
+ @classmethod
98
+ def from_string(cls, string: str) -> Properties:
99
99
  if string in ("", "[]"):
100
- return Properties()
100
+ return cls()
101
101
 
102
102
  if not (string.startswith("[") and string.endswith("]")):
103
103
  raise ValueError(f"Invalid properties string {string!r}")
@@ -113,18 +113,18 @@ class Properties(dict):
113
113
  ValueError(f"Duplicate property name {name!r}")
114
114
  props[name] = PropertyValue.from_string(string=val_str).get()
115
115
 
116
- return Properties(props)
116
+ return cls(props)
117
117
 
118
118
  def to_nbt(self) -> nbtlib.Compound:
119
119
  return nbtlib.Compound(
120
120
  {name: value.to_nbt() for name, value in sorted(super().items())})
121
121
 
122
- @staticmethod
123
- def from_nbt(nbt: nbtlib.Compound) -> Properties:
122
+ @classmethod
123
+ def from_nbt(cls, nbt: nbtlib.Compound) -> Properties:
124
124
  props = {}
125
125
  for name, value in nbt.items():
126
126
  props[name] = PropertyValue.from_nbt(nbt=value).get()
127
- return Properties(props)
127
+ return cls(props)
128
128
 
129
129
 
130
130
  class PropertyValue(ABC):
@@ -204,20 +204,20 @@ class PropertyValue(ABC):
204
204
  def to_string(self) -> str:
205
205
  return str(self)
206
206
 
207
- @staticmethod
208
- def from_string(string: str) -> PropertyValue:
207
+ @classmethod
208
+ def from_string(cls, string: str) -> PropertyValue:
209
209
  try:
210
210
  value = json.loads(string)
211
211
  except json.JSONDecodeError:
212
212
  value = string
213
- return PropertyValue.value_factory(value)
213
+ return cls.value_factory(value)
214
214
 
215
215
  def to_nbt(self) -> nbtlib.String:
216
216
  return nbtlib.String(self)
217
217
 
218
- @staticmethod
219
- def from_nbt(nbt: nbtlib.String) -> PropertyValue:
220
- return PropertyValue.from_string(str(nbt))
218
+ @classmethod
219
+ def from_nbt(cls, nbt: nbtlib.String) -> PropertyValue:
220
+ return cls.from_string(str(nbt))
221
221
 
222
222
 
223
223
  class BooleanValue(PropertyValue):
@@ -74,15 +74,15 @@ class BlockState:
74
74
  def to_string(self) -> str:
75
75
  return str(self)
76
76
 
77
- @staticmethod
78
- def from_string(string: str) -> BlockState:
77
+ @classmethod
78
+ def from_string(cls, string: str) -> BlockState:
79
79
  idx = string.find("[") # basic parsing to separate block:id[name=value]
80
80
  if idx == -1:
81
81
  id, props = string, ""
82
82
  else:
83
83
  id, props = string[:idx], string[idx:]
84
84
 
85
- state = BlockState(id)
85
+ state = cls(id)
86
86
  state._props = Properties.from_string(props)
87
87
  return state
88
88
 
@@ -93,19 +93,19 @@ class BlockState:
93
93
  nbt["Properties"] = self._props.to_nbt()
94
94
  return nbt
95
95
 
96
- @staticmethod
97
- def from_nbt(nbt: Compound) -> BlockState:
98
- state = BlockState(str(nbt["Name"]))
96
+ @classmethod
97
+ def from_nbt(cls, nbt: Compound) -> BlockState:
98
+ state = cls(str(nbt["Name"]))
99
99
  state._props = Properties.from_nbt(nbt.get("Properties", Compound()))
100
100
  return state
101
101
 
102
102
  def with_id(self, id: str) -> BlockState:
103
- state = BlockState(id)
103
+ state = type(self)(id)
104
104
  state._props = deepcopy(self._props)
105
105
  return state
106
106
 
107
107
  def with_props(self, **props: Any) -> BlockState:
108
- state = BlockState(self.id)
108
+ state = type(self)(self.id)
109
109
  new_props = deepcopy(self._props)
110
110
  for name, value in props.items():
111
111
  if value is None:
pylitematic/geometry.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ import enum
4
5
  import nbtlib
5
6
  import numpy as np
7
+ from typing import Iterator
6
8
 
7
9
 
8
10
  @dataclass(frozen=True)
@@ -12,134 +14,89 @@ class Vec3i:
12
14
  _c: int
13
15
 
14
16
  def __post_init__(self) -> None:
15
- object.__setattr__(self, "_a", self._to_int(self._a))
16
- object.__setattr__(self, "_b", self._to_int(self._b))
17
- object.__setattr__(self, "_c", self._to_int(self._c))
17
+ object.__setattr__(self, "_a", int(self._a))
18
+ object.__setattr__(self, "_b", int(self._b))
19
+ object.__setattr__(self, "_c", int(self._c))
20
+
21
+ def __getitem__(self, index: int) -> int:
22
+ return tuple(self)[index]
18
23
 
19
24
  def __str__(self) -> str:
20
25
  return str(list(self))
21
26
 
27
+ def __repr__(self) -> str:
28
+ return (
29
+ f"{type(self).__name__}(a={self._a}, b={self._b}, c={self._c})")
30
+
22
31
  def __len__(self) -> int:
23
32
  return 3
24
33
 
34
+ def __iter__(self) -> Iterator[int]:
35
+ return iter((self._a, self._b, self._c))
36
+
37
+ def __neg__(self) -> Vec3i:
38
+ return type(self)(*(-i for i in self))
39
+
40
+ def __abs__(self) -> Vec3i:
41
+ return type(self)(*(abs(i) for i in self))
42
+
43
+ def __array__(self, dtype: type | None = None, copy: bool = True):
44
+ arr = np.array(tuple(self), dtype=dtype)
45
+ return arr.copy() if copy else arr
46
+
25
47
  def __add__(self, other) -> Vec3i:
26
- arr = np.array(self)
27
- other = self._to_array(other)
28
- try:
29
- result = arr + other
30
- except Exception:
31
- return NotImplemented
32
- return type(self)(*result.astype(int))
48
+ return type(self)(*(np.array(self) + other))
33
49
 
34
50
  def __radd__(self, other) -> Vec3i:
35
51
  return self.__add__(other)
36
52
 
37
53
  def __sub__(self, other) -> Vec3i:
38
- arr = np.array(self)
39
- other = self._to_array(other)
40
- try:
41
- result = arr - other
42
- except Exception:
43
- return NotImplemented
44
- return type(self)(*result.astype(int))
54
+ return self.__add__(-other)
45
55
 
46
56
  def __rsub__(self, other) -> Vec3i:
47
- arr = np.array(self)
48
- try:
49
- result = other - arr
50
- except Exception:
51
- return NotImplemented
52
- return type(self)(*result.astype(int))
57
+ return -self.__sub__(other)
53
58
 
54
59
  def __mul__(self, other) -> Vec3i:
55
- arr = np.array(self)
56
- other = self._to_array(other)
57
- try:
58
- result = arr * other
59
- except Exception:
60
- return NotImplemented
61
- return type(self)(*result.astype(int))
60
+ return type(self)(*(np.array(self) * other))
62
61
 
63
62
  def __rmul__(self, other) -> Vec3i:
64
63
  return self.__mul__(other)
65
64
 
66
65
  def __floordiv__(self, other) -> Vec3i:
67
- arr = np.array(self)
68
- other = self._to_array(other)
69
- try:
70
- result = arr // other
71
- except Exception:
72
- return NotImplemented
73
- return type(self)(*result.astype(int))
66
+ return type(self)(*(np.array(self) // other))
74
67
 
75
68
  def __rfloordiv__(self, other) -> Vec3i:
76
- arr = np.array(self)
77
- try:
78
- result = other // arr
79
- except Exception:
80
- return NotImplemented
81
- return type(self)(*result.astype(int))
69
+ return type(self)(*(other // np.array(self)))
82
70
 
83
- def __neg__(self) -> Vec3i:
84
- return type(self)(-self._a, -self._b, -self._c)
71
+ def __truediv__(self, other) -> Vec3i:
72
+ return self.__floordiv__(other)
85
73
 
86
- def __getitem__(self, index: int) -> int:
87
- return self.to_tuple()[index]
74
+ def __rtruediv__(self, other) -> Vec3i:
75
+ return self.__rfloordiv__(other)
88
76
 
89
- def __iter__(self):
90
- return iter(self.to_tuple())
77
+ def __mod__(self, other) -> Vec3i:
78
+ return type(self)(*(np.array(self) % other))
91
79
 
92
- def __abs__(self) -> Vec3i:
93
- return type(self)(*np.abs(self))
80
+ def __rmod__(self, other) -> Vec3i:
81
+ return type(self)(*(other % np.array(self)))
82
+
83
+ def __eq__(self, other):
84
+ return np.array(self) == other
85
+
86
+ def __ne__(self, other):
87
+ return np.invert(self.__eq__(other))
94
88
 
95
89
  def __lt__(self, other):
96
- return np.array(self) < self._to_array(other)
90
+ return np.array(self) < other
97
91
 
98
92
  def __le__(self, other):
99
- return np.array(self) <= self._to_array(other)
93
+ return np.array(self) <= other
100
94
 
101
95
  def __gt__(self, other):
102
- return np.array(self) > self._to_array(other)
96
+ return np.array(self) > other
103
97
 
104
98
  def __ge__(self, other):
105
- return np.array(self) >= self._to_array(other)
106
-
107
- def __eq__(self, other):
108
- return np.array(self) == self._to_array(other)
109
-
110
- def __ne__(self, other):
111
- return np.array(self) != self._to_array(other)
112
-
113
- def __array__(self, dtype: type | None = None, copy: bool = True):
114
- arr = np.array([self._a, self._b, self._c], dtype=dtype)
115
- if copy:
116
- return arr.copy()
117
- else:
118
- return arr
119
-
120
- def _to_array(self, other):
121
- if isinstance(other, Vec3i):
122
- return np.array(other)
123
- else:
124
- return other
125
-
126
- @staticmethod
127
- def _to_int(value) -> int:
128
- if isinstance(value, (int, np.integer)):
129
- return int(value)
130
- elif isinstance(value, float):
131
- if value.is_integer():
132
- return int(value)
133
- raise TypeError(
134
- f"{type(value).__name__} value {value!r} is not"
135
- " int, numpy integer, or whole float")
136
-
137
- def to_tuple(self) -> tuple[int, int, int]:
138
- return (self._a, self._b, self._c)
139
-
140
- @classmethod
141
- def from_tuple(cls, t: tuple[int, int, int]) -> Vec3i:
142
- return cls(*t)
99
+ return np.array(self) >= other
143
100
 
144
101
  def to_nbt(self) -> nbtlib.Compound:
145
102
  return nbtlib.Compound({
@@ -172,6 +129,15 @@ class BlockPosition(Vec3i):
172
129
  return f"{type(self).__name__}(x={self.x}, y={self.y}, z={self.z})"
173
130
 
174
131
 
132
+ class Direction(BlockPosition, enum.Enum):
133
+ NORTH = 0, 0, -1
134
+ SOUTH = 0, 0, 1
135
+ WEST = -1, 0, 0
136
+ EAST = 1, 0, 0
137
+ UP = 0, 1, 0
138
+ DOWN = 0, -1, 0
139
+
140
+
175
141
  @dataclass(frozen=True)
176
142
  class Size3D(Vec3i):
177
143
 
@@ -192,16 +158,14 @@ class Size3D(Vec3i):
192
158
  f"{type(self).__name__}("
193
159
  f"width={self.width}, height={self.height}, length={self.length})")
194
160
 
195
- def end(self, axis: tuple[int,...] | int | None = None) -> BlockPosition:
196
- limit = self - np.sign(self)
197
-
198
- if axis is None:
199
- return BlockPosition(*limit)
161
+ @property
162
+ def volume(self) -> int:
163
+ return abs(self.width * self.height * self.length)
200
164
 
201
- if not isinstance(axis, tuple):
202
- axis = (axis, )
165
+ @property
166
+ def limit(self) -> BlockPosition:
167
+ return BlockPosition(*(self - self.sign))
203
168
 
204
- ret = np.zeros_like(limit, dtype=int)
205
- for ax in axis:
206
- ret[ax] = limit[ax]
207
- return BlockPosition(*ret)
169
+ @property
170
+ def sign(self) -> BlockPosition:
171
+ return BlockPosition(*np.sign(self))
pylitematic/region.py CHANGED
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from abc import ABC, abstractmethod
3
4
  from bitpacking import bitpack, bitunpack
4
5
  from functools import cached_property
6
+ from itertools import product
5
7
  import nbtlib
6
8
  import numpy as np
7
9
  import twos
@@ -21,130 +23,63 @@ class Region:
21
23
  size: tuple[int, int, int] | Size3D,
22
24
  origin: tuple[int, int, int] | BlockPosition = (0, 0, 0),
23
25
  ):
24
- self._origin: BlockPosition = BlockPosition(*origin)
25
- self._size: Size3D = Size3D(*size)
26
+ if not isinstance(size, Size3D):
27
+ size = Size3D(*size)
28
+ self._size: Size3D = size
26
29
 
27
- self._palette: list[BlockState] = [AIR]
28
- self._palette_map: dict[BlockState, int] = {AIR: 0}
30
+ if not isinstance(origin, BlockPosition):
31
+ origin = BlockPosition(*origin)
32
+ self._origin: BlockPosition = origin
33
+
34
+ self._palette: list[BlockState] = [AIR] # TODO: add clear method
35
+ self._palette_map: dict[BlockState, int] = {AIR: 0} # TODO: bind tighter to _palette
29
36
  self._blocks = np.zeros(abs(self._size), dtype=int)
30
37
 
31
- # TODO: Add support
38
+ # TODO: Add support for (tile) entities and ticks
32
39
  self._entities = nbtlib.List[nbtlib.Compound]()
33
40
  self._tile_entities = nbtlib.List[nbtlib.Compound]()
34
41
  self._block_ticks = nbtlib.List[nbtlib.Compound]()
35
42
  self._fluid_ticks = nbtlib.List[nbtlib.Compound]()
36
43
 
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)
44
+ self._local = LocalRegionView(self)
45
+ self._world = WorldRegionView(self)
46
+ self._numpy = NumpyRegionView(self)
54
47
 
55
- if isinstance(other, BlockState):
56
- matches = np.array([state == other for state in palette])
57
- return matches[self._blocks]
58
-
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]
48
+ @property
49
+ def local(self) -> LocalRegionView:
50
+ return self._local
71
51
 
72
- return np.array(self._palette, dtype=object)[indices]
52
+ @property
53
+ def world(self) -> WorldRegionView:
54
+ return self._world
73
55
 
74
- def __setitem__(self, key, value) -> None:
75
- index = self._key_to_index(key)
56
+ @property
57
+ def numpy(self) -> NumpyRegionView:
58
+ return self._numpy
76
59
 
77
- if isinstance(value, list):
78
- value = np.array(value, dtype=object)
60
+ def __contains__(self, item) -> bool:
61
+ return item in self.local
79
62
 
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]
63
+ def __eq__(self, other) -> bool:
64
+ return self.local == other
86
65
 
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")
66
+ def __ne__(self, other) -> bool:
67
+ return self.local != other
91
68
 
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")
69
+ def __getitem__(self, key):
70
+ return self.local[key]
105
71
 
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)
133
-
134
- def _from_internal(self, index: tuple[int, int, int]) -> BlockPosition:
135
- return self.lower + index
72
+ def __setitem__(self, key, value) -> None:
73
+ self.local[key] = value
136
74
 
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)
75
+ def __iter__(self) -> tuple[BlockPosition, BlockState]:
76
+ return iter(self.local)
143
77
 
144
78
  def compact_palette(self) -> None:
79
+ # TODO: determine all appropriate places to call this method
145
80
  idx = np.unique(self._blocks)
146
- # always include minecraft:air in a palette
147
81
  if 0 not in idx:
82
+ # always include minecraft:air as the first entry in the palette
148
83
  idx = np.insert(idx, 0, 0)
149
84
  index_map = {old: new for new, old in enumerate(idx)}
150
85
 
@@ -160,6 +95,7 @@ class Region:
160
95
  self._palette = palette
161
96
  self._palette_map = palette_map
162
97
 
98
+ # block state en- / decoding (NBT)
163
99
  def _bits_per_state(self) -> int:
164
100
  return max(2, (len(self._palette) - 1).bit_length())
165
101
 
@@ -190,114 +126,72 @@ class Region:
190
126
  def size(self) -> Size3D:
191
127
  return self._size
192
128
 
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
129
  @property
214
130
  def width(self) -> int:
215
- return self._size.width
131
+ return self.size.width
216
132
 
217
133
  @property
218
134
  def height(self) -> int:
219
- return self._size.height
135
+ return self.size.height
220
136
 
221
137
  @property
222
138
  def length(self) -> int:
223
- return self._size.length
139
+ return self.size.length
224
140
 
225
141
  @property
226
142
  def volume(self) -> int:
227
- return np.prod(self.shape).item()
143
+ return self.size.volume
228
144
 
229
145
  @property
230
- def block_count(self) -> int:
231
- # TODO: Add filter BlockState and rename to count()
232
- return np.count_nonzero(self._blocks)
146
+ def origin(self) -> BlockPosition:
147
+ return self._origin
233
148
 
234
149
  @property
235
- def shape(self) -> tuple[int, int, int]:
236
- return self._blocks.shape
150
+ def block_count(self) -> int:
151
+ # TODO: Add filter BlockStates / BlockIds and rename to count()
152
+ return np.sum(self != AIR).item()
237
153
 
238
- @cached_property
154
+ @property
239
155
  def lower(self) -> BlockPosition:
240
- return BlockPosition(*np.min((self.start, self.end), axis=0))
156
+ return self.local.lower
241
157
 
242
- @cached_property
158
+ @property
243
159
  def upper(self) -> BlockPosition:
244
- return BlockPosition(*np.max((self.start, self.end), axis=0))
160
+ return self.local.upper
245
161
 
246
- @cached_property
162
+ @property
247
163
  def bounds(self) -> tuple[BlockPosition, BlockPosition]:
248
- return self.lower, self.upper
164
+ return self.local.bounds
249
165
 
250
- @cached_property
251
- def global_lower(self) -> BlockPosition:
252
- return BlockPosition(*np.min((self.origin, self.limit), axis=0))
166
+ def items(self) -> Iterator[tuple[BlockPosition, BlockState]]:
167
+ return self.local.items()
253
168
 
254
- @cached_property
255
- def global_upper(self) -> BlockPosition:
256
- return BlockPosition(*np.max((self.origin, self.limit), axis=0))
169
+ def positions(self) -> Iterator[BlockPosition]:
170
+ return self.local.positions()
257
171
 
258
- @cached_property
259
- def global_bounds(self) -> tuple[BlockPosition, BlockPosition]:
260
- return self.global_lower, self.global_upper
172
+ def blocks(self) -> Iterator[BlockState]:
173
+ return self.local.blocks()
261
174
 
262
- def blocks(
263
- 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
175
+ # block position transformations
176
+ def world_to_local(self, world: BlockPosition) -> BlockPosition:
177
+ return world - self._origin
284
178
 
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
179
+ def local_to_world(self, local: BlockPosition) -> BlockPosition:
180
+ return self._origin + local
292
181
 
293
- yield pos, state
182
+ def local_to_numpy(self, local: BlockPosition) -> BlockPosition:
183
+ return BlockPosition(*self.local.position_to_index(local))
294
184
 
295
- def to_global(self, local_pos: BlockPosition) -> BlockPosition:
296
- return self._origin + local_pos
185
+ def numpy_to_local(self, index: BlockPosition) -> BlockPosition:
186
+ return self.local.index_to_position(tuple(index))
297
187
 
298
- def to_local(self, global_pos: BlockPosition) -> BlockPosition:
299
- return global_pos - self._origin
188
+ def world_to_numpy(self, world: BlockPosition) -> BlockPosition:
189
+ return BlockPosition(*self.world.position_to_index(world))
300
190
 
191
+ def numpy_to_world(self, index: BlockPosition) -> BlockPosition:
192
+ return self.world.index_to_position(tuple(index))
193
+
194
+ # NBT conversion
301
195
  def to_nbt(self) -> nbtlib.Compound:
302
196
  nbt = nbtlib.Compound()
303
197
 
@@ -315,12 +209,12 @@ class Region:
315
209
 
316
210
  return nbt
317
211
 
318
- @staticmethod
319
- def from_nbt(nbt: nbtlib.Compound) -> Region:
212
+ @classmethod
213
+ def from_nbt(cls, nbt: nbtlib.Compound) -> Region:
320
214
  pos = BlockPosition.from_nbt(nbt["Position"])
321
215
  size = Size3D.from_nbt(nbt["Size"])
322
216
 
323
- region = Region(origin=pos, size=size)
217
+ region = cls(origin=pos, size=size)
324
218
 
325
219
  region._palette = [
326
220
  BlockState.from_nbt(block) for block in nbt["BlockStatePalette"]]
@@ -333,3 +227,259 @@ class Region:
333
227
  region._fluid_ticks = nbt["PendingFluidTicks"]
334
228
 
335
229
  return region
230
+
231
+
232
+ class _RegionView(ABC):
233
+
234
+ def __init__(self, region: Region) -> None:
235
+ self.region = region
236
+ # TODO: add support for (tile) entities and ticks
237
+
238
+ @property
239
+ def _blocks(self) -> np.ndarray[int]:
240
+ return self.region._blocks
241
+
242
+ @property
243
+ def _palette(self) -> list[BlockState]:
244
+ return self.region._palette
245
+
246
+ @property
247
+ def _palette_map(self) -> dict[BlockState, int]:
248
+ return self.region._palette_map
249
+
250
+ @abstractmethod
251
+ def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
252
+ ...
253
+
254
+ @abstractmethod
255
+ def index_to_position(self, index: tuple[int, int, int]) -> BlockPosition:
256
+ ...
257
+
258
+ @abstractmethod
259
+ def _align_array(self, arr: np.ndarray) -> np.ndarray:
260
+ ...
261
+
262
+ @abstractmethod
263
+ def _transform_index(self, index):
264
+ ...
265
+
266
+ def __getitem__(self, key):
267
+ if isinstance(key, BlockPosition):
268
+ # return self.at(key) # TODO
269
+ key = tuple(key)
270
+ index = self._transform_index(key)
271
+
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]
277
+
278
+ def __setitem__(self, key, value):
279
+ if isinstance(key, BlockPosition):
280
+ # return self.set_at(key, value) # TODO
281
+ key = tuple(key)
282
+ index = self._transform_index(key)
283
+
284
+ if isinstance(value, list):
285
+ value = np.array(value, dtype=object)
286
+
287
+ if isinstance(value, BlockState):
288
+ # 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]
293
+
294
+ elif isinstance(value, np.ndarray):
295
+ if value.shape != self._blocks[index].shape:
296
+ # TODO: allow casting
297
+ raise ValueError(
298
+ "Shape mismatch between assigned array and target slice")
299
+
300
+ # look up (or add) indices for all BlockStates
301
+ unique_states, xdi = np.unique(value, return_inverse=True)
302
+ idx = []
303
+ 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])
308
+ index_array = np.array(idx, dtype=int)[xdi].reshape(value.shape)
309
+ self._blocks[index] = index_array
310
+ else:
311
+ raise TypeError(
312
+ "Value must be a BlockState or a list of BlockStates")
313
+
314
+ def __contains__(self, item) -> bool:
315
+ if isinstance(item, BlockPosition):
316
+ return all(self.lower <= item) and all(item <= self.upper)
317
+
318
+ elif isinstance(item, BlockState):
319
+ index = self._palette_map.get(item)
320
+ if index is None:
321
+ return False
322
+ return index in self._blocks
323
+ # return np.any(self._blocks == index).item()
324
+
325
+ elif isinstance(item, BlockId):
326
+ 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())
330
+
331
+ else:
332
+ return False
333
+
334
+ def __iter__(self) -> Iterator[tuple[BlockPosition, BlockState]]:
335
+ return self.items()
336
+
337
+ def items(self) -> Iterator[tuple[BlockPosition, BlockState]]:
338
+ for pos, block in zip(self.positions(), self.blocks()):
339
+ yield pos, block
340
+
341
+ def positions(self) -> Iterator[BlockPosition]:
342
+ ranges = [
343
+ range(start, stop, step)
344
+ for start, stop, step
345
+ in zip(self.origin, self.origin + self.size, self.size.sign)
346
+ ]
347
+ for z, y, x in product(*reversed(ranges)):
348
+ yield BlockPosition(x, y, z)
349
+
350
+ 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]:
354
+ yield block
355
+
356
+ def __eq__(self, other) -> np.ndarray[bool]:
357
+ palette = np.array(self._palette, dtype=object)
358
+
359
+ if isinstance(other, BlockState):
360
+ matches = np.array([state == other for state in palette])
361
+ mask = matches[self._blocks]
362
+
363
+ elif isinstance(other, BlockId):
364
+ matches = np.array([state.id == other for state in palette])
365
+ mask = matches[self._blocks]
366
+
367
+ else:
368
+ return NotImplemented
369
+
370
+ return self._align_array(mask)
371
+
372
+ def __ne__(self, other) -> np.ndarray[bool]:
373
+ return np.invert(self.__eq__(other))
374
+
375
+ property
376
+ @abstractmethod
377
+ def origin(self) -> BlockPosition:
378
+ ...
379
+
380
+ @property
381
+ @abstractmethod
382
+ def size(self) -> Size3D:
383
+ ...
384
+
385
+ @cached_property
386
+ def limit(self) -> BlockPosition:
387
+ return self.origin + self.size.limit
388
+
389
+ @cached_property
390
+ def lower(self) -> BlockPosition:
391
+ return BlockPosition(*np.min((self.origin, self.limit), axis=0))
392
+
393
+ @cached_property
394
+ def upper(self) -> BlockPosition:
395
+ return BlockPosition(*np.max((self.origin, self.limit), axis=0))
396
+
397
+ @property
398
+ def bounds(self) -> tuple[BlockPosition, BlockPosition]:
399
+ return self.lower, self.upper
400
+
401
+
402
+ class NumpyRegionView(_RegionView):
403
+
404
+ @property
405
+ def origin(self) -> BlockPosition:
406
+ return BlockPosition(0, 0, 0)
407
+
408
+ @property
409
+ def size(self) -> Size3D:
410
+ return abs(self.region._size)
411
+
412
+ def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
413
+ return tuple(pos)
414
+
415
+ def index_to_position(self, index: tuple[int, int, int]) -> BlockPosition:
416
+ return BlockPosition(*index)
417
+
418
+ def _align_array(self, arr: np.ndarray) -> np.ndarray:
419
+ return arr
420
+
421
+ def _transform_index(self, index):
422
+ return index
423
+
424
+
425
+ class _OrientedView(_RegionView):
426
+
427
+ @property
428
+ def size(self) -> Size3D:
429
+ return self.region._size
430
+
431
+ @cached_property
432
+ def negative_axes(self) -> tuple[int,...]:
433
+ return tuple(np.argwhere(self.size < 0).flatten().tolist())
434
+
435
+ def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
436
+ return pos - self.lower
437
+
438
+ def index_to_position(self, index: tuple[int, int, int]) -> BlockPosition:
439
+ return self.lower + index
440
+
441
+ def _align_array(self, arr: np.ndarray) -> np.ndarray:
442
+ return np.flip(arr, axis=self.negative_axes)
443
+
444
+ def _transform_index(self, key):
445
+ if isinstance(key, (int, np.integer, slice, type(Ellipsis))):
446
+ key = (key,)
447
+
448
+ if isinstance(key, tuple):
449
+ key = list(key)
450
+ for i, k in enumerate(key):
451
+ offset = self.lower[i]
452
+ if isinstance(k, (int, np.integer)):
453
+ key[i] = k - offset
454
+ elif isinstance(k, slice):
455
+ start = k.start - offset if k.start is not None else None
456
+ stop = k.stop - offset if k.stop is not None else None
457
+ key[i] = slice(start, stop, k.step)
458
+ else:
459
+ # Ellipsis
460
+ key[i] = k
461
+ return tuple(key)
462
+
463
+ elif isinstance(key, np.ndarray) and key.dtype == bool:
464
+ # boolean indexing
465
+ key = self._align_array(key)
466
+ if key.shape != self._blocks.shape:
467
+ raise IndexError("Boolean index must match region shape.")
468
+ return key
469
+
470
+ else:
471
+ return key
472
+
473
+
474
+ class LocalRegionView(_OrientedView):
475
+
476
+ @property
477
+ def origin(self) -> BlockPosition:
478
+ return BlockPosition(0, 0, 0)
479
+
480
+
481
+ class WorldRegionView(_OrientedView):
482
+
483
+ @property
484
+ def origin(self) -> BlockPosition:
485
+ return self.region._origin
@@ -50,8 +50,8 @@ class ResourceLocation:
50
50
  def to_string(self) -> str:
51
51
  return str(self)
52
52
 
53
- @staticmethod
54
- def from_string(string: str) -> ResourceLocation:
53
+ @classmethod
54
+ def from_string(cls, string: str) -> ResourceLocation:
55
55
  match = LOCATION_PATTERN.fullmatch(string)
56
56
  if not match:
57
57
  raise ValueError(f"Invalid resource location string {string!r}")
@@ -59,14 +59,14 @@ class ResourceLocation:
59
59
  namespace = match.group("namespace")
60
60
  path = match.group("path")
61
61
 
62
- return ResourceLocation(path=path, namespace=namespace)
62
+ return cls(path=path, namespace=namespace)
63
63
 
64
64
  def to_nbt(self) -> nbtlib.String:
65
65
  return nbtlib.String(self)
66
66
 
67
- @staticmethod
68
- def from_nbt(nbt: nbtlib.String) -> ResourceLocation:
69
- return ResourceLocation.from_string(str(nbt))
67
+ @classmethod
68
+ def from_nbt(cls, nbt: nbtlib.String) -> ResourceLocation:
69
+ return cls.from_string(str(nbt))
70
70
 
71
71
 
72
72
  class BlockId(ResourceLocation):
pylitematic/schematic.py CHANGED
@@ -70,6 +70,7 @@ class Schematic:
70
70
  self._created_at = round(time.time() * 1000)
71
71
  self._modified_at = self._created_at
72
72
 
73
+ # TODO: use packaging.version.Version
73
74
  self.version_major = version_major
74
75
  self.version_minor = version_minor
75
76
  self.mc_version = mc_version
@@ -104,10 +105,11 @@ class Schematic:
104
105
 
105
106
  @property
106
107
  def bounds(self) -> tuple[BlockPosition, BlockPosition]:
108
+ # TODO: make cached and update on region add / remove
107
109
  lowers = []
108
110
  uppers = []
109
111
  for reg in self._regions.values():
110
- lower, upper = reg.global_bounds
112
+ lower, upper = reg.world.bounds
111
113
  lowers.append(lower)
112
114
  uppers.append(upper)
113
115
  return (
@@ -142,9 +144,11 @@ class Schematic:
142
144
 
143
145
  def add_region(self, name: str, region: Region) -> None:
144
146
  self._regions[name] = region
147
+ # TODO: re-calculate bounding box
145
148
 
146
149
  def remove_region(self, name: str) -> Region:
147
150
  return self._regions.pop(name)
151
+ # TODO: re-calculate bounding box
148
152
 
149
153
  @property
150
154
  def created_at(self) -> datetime:
@@ -158,15 +162,14 @@ class Schematic:
158
162
  file = nbtlib.File(self.to_nbt())
159
163
  file.save(path, gzipped=True, byteorder="big")
160
164
 
161
- @staticmethod
162
- def load(path: pathlib.Path | str) -> Schematic:
165
+ @classmethod
166
+ def load(cls, path: pathlib.Path | str) -> Schematic:
163
167
  if isinstance(path, str):
164
168
  path = pathlib.Path(path)
165
169
  nbt = nbtlib.File.load(path.expanduser(), True)
166
- return Schematic.from_nbt(nbt)
170
+ return cls.from_nbt(nbt)
167
171
 
168
172
  def to_nbt(self) -> nbtlib.Compound:
169
- # self._update()
170
173
  nbt = nbtlib.Compound()
171
174
 
172
175
  # meta data
@@ -202,8 +205,8 @@ class Schematic:
202
205
 
203
206
  return nbt
204
207
 
205
- @staticmethod
206
- def from_nbt(nbt: nbtlib.Compound) -> Schematic:
208
+ @classmethod
209
+ def from_nbt(cls, nbt: nbtlib.Compound) -> Schematic:
207
210
  # meta data
208
211
  try:
209
212
  meta = nbt["Metadata"]
@@ -247,7 +250,7 @@ class Schematic:
247
250
 
248
251
  mc_version = nbt.get("MinecraftDataVersion")
249
252
 
250
- schem = Schematic(
253
+ schem = cls(
251
254
  name=name,
252
255
  author=author,
253
256
  description=desc,
pylitematic/test.py CHANGED
@@ -45,27 +45,28 @@ air = BlockState("air")
45
45
  stone = BlockState("stone")
46
46
  dirt = BlockState("dirt")
47
47
  grass = BlockState("grass_block")
48
- cobble = BlockState("mossy_cobblestone")
48
+ cobble = BlockState("cobblestone")
49
+ mossy_cobble = BlockState("mossy_cobblestone")
49
50
  snow = BlockState("snow_block")
50
51
  pumpkin = BlockState("carved_pumpkin", facing="west")
51
52
 
52
53
  ground = Region(size=Size3D(16, 9, 16), origin=BlockPosition(0, 0, 0))
53
- ground[:,:5,:] = stone
54
- ground[:,5:8,:] = dirt
55
- ground[:,8:,:] = grass
54
+ ground.local[:,:5,:] = stone
55
+ ground.local[:,5:8,:] = dirt
56
+ ground.local[:,8:,:] = grass
56
57
 
57
58
  boulder = Region(size=(4, 4, 4), origin=ground.origin+[6, ground.height, 6])
58
- boulder[:] = cobble
59
+ boulder[:] = mossy_cobble
60
+ boulder.numpy[:,-2:,:] = cobble
59
61
 
60
62
  # snow_man = Region(size=(1, 3, 1), origin=boulder.origin+[1, boulder.height, 1])
61
- snow_man = Region(size=(-1, -3, -1), origin=boulder.origin+[1, boulder.height+2, 1])
62
- snow_man[:] = snow
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]]]
63
66
  snow_man[0,snow_man.upper.y,0] = pumpkin
64
67
 
65
- schem = Schematic(name="scene", author="Boscawinks", description="A simple scene")
68
+ schem = Schematic(name="a_scene", author="Boscawinks", description="A simple scene")
66
69
  schem.add_region("ground", ground)
67
70
  schem.add_region("boulder", boulder)
68
71
  schem.add_region("snow_man", snow_man)
69
72
  schem.save(f"/mnt/d/minecraft/schematics/Litematica/test/{schem.name}.litematic")
70
-
71
- print(snow_man == snow)
@@ -1,13 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylitematic
3
- Version: 0.0.3
3
+ Version: 0.0.4
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
7
7
  Project-URL: Homepage, https://github.com/boscawinks/pylitematic
8
8
  Project-URL: Repository, https://github.com/boscawinks/pylitematic
9
9
  Project-URL: Issues, https://github.com/boscawinks/pylitematic/issues
10
+ Classifier: Development Status :: 2 - Pre-Alpha
10
11
  Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Utilities
11
14
  Requires-Python: >=3.10.12
12
15
  Description-Content-Type: text/markdown
13
16
  Requires-Dist: bitpacking>=0.1.0
@@ -0,0 +1,12 @@
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,,
@@ -1,12 +0,0 @@
1
- pylitematic/__init__.py,sha256=-yLTtTzD7kRpeKMbUTYNy67zkFkxUP8V1DnPXhS3ZFY,202
2
- pylitematic/block_property.py,sha256=4wwsQrxyNGDcrktpd4tD0Vzv-a1OxWYMj0H05GDiT-I,7739
3
- pylitematic/block_state.py,sha256=tMRoY6jPtt2M85DR9U9dnQh2Ukp6pP_65xXzMIFeUEo,3546
4
- pylitematic/geometry.py,sha256=kt2buIJovAymAtaGQYmmWj7ypOt1xI8s--LHkbTPUlM,5399
5
- pylitematic/region.py,sha256=0bUcK9oVyhNDzfxTjZmVLwMddrIdxvJX-cXkWL8BRSE,11097
6
- pylitematic/resource_location.py,sha256=gHVE1RTRHtc8DWSlXa0WUom68tRlJy8LU5dOzuONV98,2186
7
- pylitematic/schematic.py,sha256=SLz4NQNBJkLXV9Oj0fVOE-daP4MRsdYAnnl1vKzJv_Q,7928
8
- pylitematic/test.py,sha256=9uch5L58o9QVJI50jUcbuulEswYzDgtYnRiIP991jFo,3162
9
- pylitematic-0.0.3.dist-info/METADATA,sha256=ES-4UDWi6DQU6fCSNrgf5FLMDX_jh1g7k6kx9fmiX70,1033
10
- pylitematic-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- pylitematic-0.0.3.dist-info/top_level.txt,sha256=sYUxm6O7Dh5TzuP-kPFe2FHJWUuwHFO69vN2VBiEG4A,12
12
- pylitematic-0.0.3.dist-info/RECORD,,