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 +2 -3
- pylitematic/block_palette.py +129 -0
- pylitematic/block_state.py +26 -4
- pylitematic/geometry.py +1 -1
- pylitematic/property_cache.py +52 -0
- pylitematic/region.py +256 -107
- pylitematic/resource_location.py +0 -4
- pylitematic/schematic.py +13 -5
- pylitematic/test.py +61 -54
- {pylitematic-0.0.4.dist-info → pylitematic-0.0.5.dist-info}/METADATA +1 -1
- pylitematic-0.0.5.dist-info/RECORD +14 -0
- pylitematic-0.0.4.dist-info/RECORD +0 -12
- {pylitematic-0.0.4.dist-info → pylitematic-0.0.5.dist-info}/WHEEL +0 -0
- {pylitematic-0.0.4.dist-info → pylitematic-0.0.5.dist-info}/top_level.txt +0 -0
pylitematic/__init__.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
__version__ = "0.0.
|
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
|
pylitematic/block_state.py
CHANGED
@@ -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
|
-
|
46
|
-
|
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
@@ -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
|
-
|
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 .
|
13
|
-
from .
|
14
|
-
from .
|
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:
|
35
|
-
self.
|
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.
|
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
|
64
|
-
return self.
|
69
|
+
def __lt__(self, other) -> np.ndarray[bool]:
|
70
|
+
return self._view < other
|
65
71
|
|
66
|
-
def
|
67
|
-
return self.
|
72
|
+
def __gt__(self, other) -> np.ndarray[bool]:
|
73
|
+
return self._view > other
|
68
74
|
|
69
75
|
def __getitem__(self, key):
|
70
|
-
return self.
|
76
|
+
return self._view[key]
|
71
77
|
|
72
78
|
def __setitem__(self, key, value) -> None:
|
73
|
-
self.
|
79
|
+
self._view[key] = value
|
74
80
|
|
75
81
|
def __iter__(self) -> tuple[BlockPosition, BlockState]:
|
76
|
-
return iter(self.
|
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
|
-
|
87
|
-
|
88
|
-
|
84
|
+
def clear(self) -> None:
|
85
|
+
self._palette.clear()
|
86
|
+
self._index_array = np.zeros(abs(self._size), dtype=int)
|
89
87
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
self.
|
88
|
+
self._entities.clear()
|
89
|
+
self._tile_entities.clear()
|
90
|
+
self._block_ticks.clear()
|
91
|
+
self._fluid_ticks.clear()
|
94
92
|
|
95
|
-
|
96
|
-
self
|
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.
|
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.
|
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.
|
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
|
-
@
|
150
|
-
def
|
151
|
-
|
152
|
-
|
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.
|
196
|
+
return self._view.items()
|
168
197
|
|
169
198
|
def positions(self) -> Iterator[BlockPosition]:
|
170
|
-
return self.
|
199
|
+
return self._view.positions()
|
171
200
|
|
172
201
|
def blocks(self) -> Iterator[BlockState]:
|
173
|
-
return self.
|
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
|
-
|
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.
|
220
|
-
|
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
|
240
|
-
return self.region.
|
305
|
+
def _index_array(self) -> np.ndarray[int]:
|
306
|
+
return self.region._index_array
|
241
307
|
|
242
308
|
@property
|
243
|
-
def _palette(self) ->
|
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.
|
273
|
-
|
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.
|
290
|
-
self.
|
291
|
-
|
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.
|
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.
|
305
|
-
self.
|
306
|
-
|
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.
|
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
|
-
|
320
|
-
if index is None:
|
390
|
+
if not item in self._palette:
|
321
391
|
return False
|
322
|
-
return
|
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
|
-
|
328
|
-
bs
|
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.
|
352
|
-
|
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
|
357
|
-
|
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(
|
360
|
-
|
361
|
-
mask = matches[self._blocks]
|
453
|
+
if isinstance(mask, (BlockState | BlockId)):
|
454
|
+
mask = self == mask
|
362
455
|
|
363
|
-
|
364
|
-
|
365
|
-
mask =
|
456
|
+
self[mask] = x
|
457
|
+
if y is not None:
|
458
|
+
self[np.invert(mask)] = y
|
366
459
|
|
367
|
-
|
368
|
-
|
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
|
373
|
-
return
|
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
|
-
@
|
531
|
+
@property
|
386
532
|
def limit(self) -> BlockPosition:
|
387
533
|
return self.origin + self.size.limit
|
388
534
|
|
389
|
-
@
|
535
|
+
@property
|
390
536
|
def lower(self) -> BlockPosition:
|
391
537
|
return BlockPosition(*np.min((self.origin, self.limit), axis=0))
|
392
538
|
|
393
|
-
@
|
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
|
-
@
|
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.
|
615
|
+
if key.shape != self._index_array.shape:
|
467
616
|
raise IndexError("Boolean index must match region shape.")
|
468
617
|
return key
|
469
618
|
|
pylitematic/resource_location.py
CHANGED
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(
|
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.
|
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
|
-
|
2
|
-
|
3
|
-
|
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)
|
54
|
-
ground.local[:,:5,:] = stone
|
55
|
-
ground.local[:,5:8,:] = dirt
|
56
|
-
ground
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
snow_man
|
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
|
-
|
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.
|
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"))
|
@@ -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,,
|
File without changes
|
File without changes
|