pylitematic 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pylitematic/__init__.py CHANGED
@@ -1,7 +1,6 @@
1
- __version__ = "0.0.3"
1
+ __version__ = "0.0.5"
2
2
 
3
- from .block_state import BlockState
4
- from .geometry import BlockPosition, Size3D
3
+ from .block_state import AIR, BlockId, BlockState
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
@@ -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):
@@ -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):
@@ -74,15 +93,15 @@ class BlockState:
74
93
  def to_string(self) -> str:
75
94
  return str(self)
76
95
 
77
- @staticmethod
78
- def from_string(string: str) -> BlockState:
96
+ @classmethod
97
+ def from_string(cls, string: str) -> BlockState:
79
98
  idx = string.find("[") # basic parsing to separate block:id[name=value]
80
99
  if idx == -1:
81
100
  id, props = string, ""
82
101
  else:
83
102
  id, props = string[:idx], string[idx:]
84
103
 
85
- state = BlockState(id)
104
+ state = cls(id)
86
105
  state._props = Properties.from_string(props)
87
106
  return state
88
107
 
@@ -93,19 +112,19 @@ class BlockState:
93
112
  nbt["Properties"] = self._props.to_nbt()
94
113
  return nbt
95
114
 
96
- @staticmethod
97
- def from_nbt(nbt: Compound) -> BlockState:
98
- state = BlockState(str(nbt["Name"]))
115
+ @classmethod
116
+ def from_nbt(cls, nbt: Compound) -> BlockState:
117
+ state = cls(str(nbt["Name"]))
99
118
  state._props = Properties.from_nbt(nbt.get("Properties", Compound()))
100
119
  return state
101
120
 
102
121
  def with_id(self, id: str) -> BlockState:
103
- state = BlockState(id)
122
+ state = type(self)(id)
104
123
  state._props = deepcopy(self._props)
105
124
  return state
106
125
 
107
126
  def with_props(self, **props: Any) -> BlockState:
108
- state = BlockState(self.id)
127
+ state = type(self)(self.id)
109
128
  new_props = deepcopy(self._props)
110
129
  for name, value in props.items():
111
130
  if value is None:
@@ -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
@@ -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 type(self)(*(np.array(self) - 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))
@@ -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()