litematica-render 0.1.0__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.
- litematic/__init__.py +34 -0
- litematic/parse/__init__.py +1 -0
- litematic/parse/errors.py +13 -0
- litematic/parse/model.py +225 -0
- litematic/parse/nbt.py +167 -0
- litematic/parse/parser.py +317 -0
- litematic/render/__init__.py +134 -0
- litematic/render/errors.py +9 -0
- litematic/render/pack/__init__.py +7 -0
- litematic/render/pack/blockstate.py +111 -0
- litematic/render/pack/compile.py +423 -0
- litematic/render/pack/elements.py +421 -0
- litematic/render/pack/entities.py +181 -0
- litematic/render/pack/extract.py +243 -0
- litematic/render/pack/loader.py +186 -0
- litematic/render/pack/resolve.py +344 -0
- litematic/render/pack/stack.py +83 -0
- litematic/render/pack/texture.py +347 -0
- litematic/render/png.py +54 -0
- litematic/render/quads.py +106 -0
- litematic/render/scene/__init__.py +6 -0
- litematic/render/scene/isometric.py +579 -0
- litematic/render/scene/painter.py +395 -0
- litematic/render/shapes/__init__.py +6 -0
- litematic/render/shapes/boxes.py +290 -0
- litematic/render/shapes/families.py +357 -0
- litematica_render-0.1.0.dist-info/METADATA +52 -0
- litematica_render-0.1.0.dist-info/RECORD +30 -0
- litematica_render-0.1.0.dist-info/WHEEL +4 -0
- litematica_render-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Per-block state compilation: variants, multipart parts, chests, texkeys."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from litematic.render.errors import RenderError
|
|
9
|
+
from litematic.render.pack.blockstate import _parse_blockstate
|
|
10
|
+
from litematic.render.pack.elements import (
|
|
11
|
+
_compile_elements,
|
|
12
|
+
_unit_cube_occl,
|
|
13
|
+
)
|
|
14
|
+
from litematic.render.pack.entities import chest_quads
|
|
15
|
+
from litematic.render.pack.resolve import _tint_for
|
|
16
|
+
from litematic.render.pack.stack import PackStack
|
|
17
|
+
from litematic.render.pack.texture import (
|
|
18
|
+
_apply_tint,
|
|
19
|
+
_Budget,
|
|
20
|
+
_cached_model,
|
|
21
|
+
_load_texture,
|
|
22
|
+
_register_array,
|
|
23
|
+
_register_builtin_texture,
|
|
24
|
+
_resolve_ref,
|
|
25
|
+
_texture_key,
|
|
26
|
+
)
|
|
27
|
+
from litematic.render.quads import _redstone_tint, sort_quads, unit_cube_quads
|
|
28
|
+
|
|
29
|
+
_QUAD_FUSE = 256
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_block(
|
|
34
|
+
stack: PackStack,
|
|
35
|
+
state_name: str,
|
|
36
|
+
block_id: str,
|
|
37
|
+
model_cache: dict[str, dict[str, Any] | None],
|
|
38
|
+
texture_cache: dict[str, np.ndarray | None],
|
|
39
|
+
tex_arrays: dict[str, np.ndarray],
|
|
40
|
+
mquad_cache: dict[str, str | None],
|
|
41
|
+
mquad_out: dict[str, np.ndarray],
|
|
42
|
+
budget: _Budget,
|
|
43
|
+
) -> tuple[dict[str, tuple[np.ndarray, bool]], str | None]:
|
|
44
|
+
"""Extract variant quads plus the block's multipart part records."""
|
|
45
|
+
raw = stack.read(state_name)
|
|
46
|
+
if raw is None: # unreachable: the path came from the stack enumeration
|
|
47
|
+
raise RenderError(f"extract: missing blockstate: {state_name}")
|
|
48
|
+
state = json.loads(raw)
|
|
49
|
+
variants, parts, model_refs = _parse_blockstate(state)
|
|
50
|
+
ns = state_name.split("/")[1]
|
|
51
|
+
result: dict[str, tuple[np.ndarray, bool]] = {}
|
|
52
|
+
for state_key, applies in variants:
|
|
53
|
+
quads: list[np.ndarray] = []
|
|
54
|
+
for variant in applies:
|
|
55
|
+
x_rot = int(variant.get("x", 0))
|
|
56
|
+
y_rot = int(variant.get("y", 0))
|
|
57
|
+
uvlock = bool(variant.get("uvlock", False))
|
|
58
|
+
model = _cached_model(stack, ns, variant["model"], model_cache)
|
|
59
|
+
if model is None:
|
|
60
|
+
continue
|
|
61
|
+
compiled = _variant_quads(
|
|
62
|
+
stack,
|
|
63
|
+
ns,
|
|
64
|
+
model,
|
|
65
|
+
block_id,
|
|
66
|
+
state_key,
|
|
67
|
+
x_rot,
|
|
68
|
+
y_rot,
|
|
69
|
+
uvlock,
|
|
70
|
+
texture_cache,
|
|
71
|
+
tex_arrays,
|
|
72
|
+
budget,
|
|
73
|
+
)
|
|
74
|
+
if compiled is not None:
|
|
75
|
+
quads.append(compiled)
|
|
76
|
+
if not quads:
|
|
77
|
+
continue
|
|
78
|
+
merged = quads[0] if len(quads) == 1 else np.concatenate(quads)
|
|
79
|
+
if len(merged) > _QUAD_FUSE:
|
|
80
|
+
continue
|
|
81
|
+
result[state_key] = (merged, _unit_cube_occl(merged, tex_arrays))
|
|
82
|
+
parts_json = _extract_parts(
|
|
83
|
+
stack,
|
|
84
|
+
ns,
|
|
85
|
+
block_id,
|
|
86
|
+
parts,
|
|
87
|
+
model_cache,
|
|
88
|
+
texture_cache,
|
|
89
|
+
tex_arrays,
|
|
90
|
+
mquad_cache,
|
|
91
|
+
mquad_out,
|
|
92
|
+
budget,
|
|
93
|
+
)
|
|
94
|
+
expanded = _chest_states(stack, ns, block_id, texture_cache, tex_arrays, budget)
|
|
95
|
+
if expanded:
|
|
96
|
+
# The expanded facing/type states supersede variant stubs: a stub
|
|
97
|
+
# key with more property pairs would otherwise out-specify them.
|
|
98
|
+
result = expanded
|
|
99
|
+
if not result and parts_json is None:
|
|
100
|
+
# No pack geometry (missing models, builtin-cut chains): resolve
|
|
101
|
+
# the builtin layer's texture per spec 5.3 so compose's state-aware
|
|
102
|
+
# builtin step can backfill it under the canonical block key.
|
|
103
|
+
_register_builtin_texture(
|
|
104
|
+
stack, ns, block_id, model_refs, model_cache,
|
|
105
|
+
texture_cache, tex_arrays, budget,
|
|
106
|
+
)
|
|
107
|
+
return result, parts_json
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _extract_parts(
|
|
112
|
+
stack: PackStack,
|
|
113
|
+
ns: str,
|
|
114
|
+
block_id: str,
|
|
115
|
+
parts: list[dict[str, Any]] | None,
|
|
116
|
+
model_cache: dict[str, dict[str, Any] | None],
|
|
117
|
+
texture_cache: dict[str, np.ndarray | None],
|
|
118
|
+
tex_arrays: dict[str, np.ndarray],
|
|
119
|
+
mquad_cache: dict[str, str | None],
|
|
120
|
+
mquad_out: dict[str, np.ndarray],
|
|
121
|
+
budget: _Budget,
|
|
122
|
+
) -> str | None:
|
|
123
|
+
"""Compile multipart part records into lazily evaluated npz entries.
|
|
124
|
+
|
|
125
|
+
Each part's geometry compiles once per (model, rotation) and the raw
|
|
126
|
+
condition tree ships alongside it; render-time evaluation against the
|
|
127
|
+
live properties replaces key enumeration entirely.
|
|
128
|
+
"""
|
|
129
|
+
if parts is None:
|
|
130
|
+
return None
|
|
131
|
+
records: list[dict[str, Any]] = []
|
|
132
|
+
for part in parts:
|
|
133
|
+
subkey = _model_quads_key(
|
|
134
|
+
stack,
|
|
135
|
+
ns,
|
|
136
|
+
part["model"],
|
|
137
|
+
part["x"],
|
|
138
|
+
part["y"],
|
|
139
|
+
part["uvlock"],
|
|
140
|
+
block_id,
|
|
141
|
+
model_cache,
|
|
142
|
+
texture_cache,
|
|
143
|
+
tex_arrays,
|
|
144
|
+
mquad_cache,
|
|
145
|
+
mquad_out,
|
|
146
|
+
budget,
|
|
147
|
+
)
|
|
148
|
+
if subkey is not None:
|
|
149
|
+
records.append({"when": part["when"], "quads": subkey})
|
|
150
|
+
if not records:
|
|
151
|
+
return None
|
|
152
|
+
if block_id.partition(":")[2] == "redstone_wire":
|
|
153
|
+
# Part quads ship untinted (the dust tint depends on the live
|
|
154
|
+
# power); register every power variant of their textures so the
|
|
155
|
+
# lookup-side texkey rewrite always finds the tinted array.
|
|
156
|
+
bases = {
|
|
157
|
+
str(key).partition("@")[0]
|
|
158
|
+
for subkey in (r["quads"] for r in records)
|
|
159
|
+
for key in mquad_out[subkey]["texkey"]
|
|
160
|
+
}
|
|
161
|
+
for base in sorted(bases):
|
|
162
|
+
raw = tex_arrays.get(base)
|
|
163
|
+
if raw is None:
|
|
164
|
+
continue
|
|
165
|
+
for power in range(16):
|
|
166
|
+
tint = _redstone_tint(power)
|
|
167
|
+
tex_arrays.setdefault(
|
|
168
|
+
f"{base}@{tint[0]},{tint[1]},{tint[2]}",
|
|
169
|
+
_apply_tint(raw, tint),
|
|
170
|
+
)
|
|
171
|
+
return json.dumps({"parts": records})
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _model_quads_key(
|
|
175
|
+
stack: PackStack,
|
|
176
|
+
ns: str,
|
|
177
|
+
ref: str,
|
|
178
|
+
x_rot: int,
|
|
179
|
+
y_rot: int,
|
|
180
|
+
uvlock: bool,
|
|
181
|
+
block_id: str,
|
|
182
|
+
model_cache: dict[str, dict[str, Any] | None],
|
|
183
|
+
texture_cache: dict[str, np.ndarray | None],
|
|
184
|
+
tex_arrays: dict[str, np.ndarray],
|
|
185
|
+
mquad_cache: dict[str, str | None],
|
|
186
|
+
mquad_out: dict[str, np.ndarray],
|
|
187
|
+
budget: _Budget,
|
|
188
|
+
) -> str | None:
|
|
189
|
+
"""Compile one part model's quads once, keyed by (ns, ref, rotation)."""
|
|
190
|
+
cache_key = f"{ns}|{ref}|x{x_rot}|y{y_rot}|u{int(uvlock)}"
|
|
191
|
+
if cache_key in mquad_cache:
|
|
192
|
+
return mquad_cache[cache_key]
|
|
193
|
+
subkey = cache_key
|
|
194
|
+
model = _cached_model(stack, ns, ref, model_cache)
|
|
195
|
+
quads: np.ndarray | None = None
|
|
196
|
+
if model is not None and not model.get("builtin_cut"):
|
|
197
|
+
if model["elements"] is not None:
|
|
198
|
+
|
|
199
|
+
def texkey_for(
|
|
200
|
+
face: dict[str, Any], textures: dict[str, str], face_ns: str
|
|
201
|
+
) -> str | None:
|
|
202
|
+
return _element_texkey(
|
|
203
|
+
stack,
|
|
204
|
+
face_ns,
|
|
205
|
+
face,
|
|
206
|
+
textures,
|
|
207
|
+
block_id,
|
|
208
|
+
"",
|
|
209
|
+
texture_cache,
|
|
210
|
+
tex_arrays,
|
|
211
|
+
budget,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
quads = _compile_elements(
|
|
215
|
+
model["elements"],
|
|
216
|
+
model["textures"],
|
|
217
|
+
ns,
|
|
218
|
+
x_rot,
|
|
219
|
+
y_rot,
|
|
220
|
+
texkey_for,
|
|
221
|
+
uvlock=uvlock,
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
quads = _stub_quads(
|
|
225
|
+
stack,
|
|
226
|
+
ns,
|
|
227
|
+
model,
|
|
228
|
+
block_id,
|
|
229
|
+
"",
|
|
230
|
+
texture_cache,
|
|
231
|
+
tex_arrays,
|
|
232
|
+
budget,
|
|
233
|
+
)
|
|
234
|
+
if quads is None or not len(quads):
|
|
235
|
+
mquad_cache[cache_key] = None
|
|
236
|
+
return None
|
|
237
|
+
mquad_out[subkey] = sort_quads(quads)
|
|
238
|
+
mquad_cache[cache_key] = subkey
|
|
239
|
+
return subkey
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _variant_quads(
|
|
244
|
+
stack: PackStack,
|
|
245
|
+
ns: str,
|
|
246
|
+
model: dict[str, Any],
|
|
247
|
+
block_id: str,
|
|
248
|
+
state_key: str,
|
|
249
|
+
x_rot: int,
|
|
250
|
+
y_rot: int,
|
|
251
|
+
uvlock: bool,
|
|
252
|
+
texture_cache: dict[str, np.ndarray | None],
|
|
253
|
+
tex_arrays: dict[str, np.ndarray],
|
|
254
|
+
budget: _Budget,
|
|
255
|
+
) -> np.ndarray | None:
|
|
256
|
+
"""Compile one variant's quads: element boxes, else a stub unit cube."""
|
|
257
|
+
if model["elements"] is None:
|
|
258
|
+
if model.get("builtin_cut"):
|
|
259
|
+
# Chains terminated by a builtin/* parent (chests, heads) have
|
|
260
|
+
# no pack geometry: defer to the builtin shape table.
|
|
261
|
+
return None
|
|
262
|
+
return _stub_quads(
|
|
263
|
+
stack,
|
|
264
|
+
ns,
|
|
265
|
+
model,
|
|
266
|
+
block_id,
|
|
267
|
+
state_key,
|
|
268
|
+
texture_cache,
|
|
269
|
+
tex_arrays,
|
|
270
|
+
budget,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def texkey_for(
|
|
274
|
+
face: dict[str, Any], textures: dict[str, str], face_ns: str
|
|
275
|
+
) -> str | None:
|
|
276
|
+
return _element_texkey(
|
|
277
|
+
stack,
|
|
278
|
+
face_ns,
|
|
279
|
+
face,
|
|
280
|
+
textures,
|
|
281
|
+
block_id,
|
|
282
|
+
state_key,
|
|
283
|
+
texture_cache,
|
|
284
|
+
tex_arrays,
|
|
285
|
+
budget,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return _compile_elements(
|
|
289
|
+
model["elements"],
|
|
290
|
+
model["textures"],
|
|
291
|
+
ns,
|
|
292
|
+
x_rot,
|
|
293
|
+
y_rot,
|
|
294
|
+
texkey_for,
|
|
295
|
+
uvlock=uvlock,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
_CHEST_STATE_KEYS = [
|
|
301
|
+
f"facing={facing},type={kind}"
|
|
302
|
+
for facing in ("north", "south", "east", "west")
|
|
303
|
+
for kind in ("single", "left", "right")
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
# Vanilla ender chests have no type property: expand facing only.
|
|
307
|
+
_ENDER_CHEST_STATE_KEYS = [
|
|
308
|
+
f"facing={facing}" for facing in ("north", "south", "east", "west")
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _chest_states(
|
|
314
|
+
stack: PackStack,
|
|
315
|
+
ns: str,
|
|
316
|
+
block_id: str,
|
|
317
|
+
texture_cache: dict[str, np.ndarray | None],
|
|
318
|
+
tex_arrays: dict[str, np.ndarray],
|
|
319
|
+
budget: _Budget,
|
|
320
|
+
) -> dict[str, tuple[np.ndarray, bool]]:
|
|
321
|
+
"""Expand an element-less chest-family block into facing/type states.
|
|
322
|
+
|
|
323
|
+
Vanilla ships chest geometry only through the block-entity renderer:
|
|
324
|
+
the blockstate never enumerates facing/type variants (a bare empty
|
|
325
|
+
variant at best), so the entity textures carry the model and the state
|
|
326
|
+
domain is expanded here.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
def resolve(tex_ref: str) -> str | None:
|
|
330
|
+
tex_key = _texture_key(tex_ref, ns)
|
|
331
|
+
raw = _load_texture(stack, ns, tex_ref, tex_key, texture_cache, budget)
|
|
332
|
+
if raw is None:
|
|
333
|
+
return None
|
|
334
|
+
return _register_array(raw, tex_key, None, tex_arrays)
|
|
335
|
+
|
|
336
|
+
if chest_quads(block_id, "", resolve) is None:
|
|
337
|
+
return {}
|
|
338
|
+
stem = block_id.partition(":")[2]
|
|
339
|
+
keys = _ENDER_CHEST_STATE_KEYS if stem == "ender_chest" else _CHEST_STATE_KEYS
|
|
340
|
+
states: dict[str, tuple[np.ndarray, bool]] = {}
|
|
341
|
+
for key in keys:
|
|
342
|
+
quads = chest_quads(block_id, key, resolve)
|
|
343
|
+
if quads is not None:
|
|
344
|
+
states[key] = (quads, False)
|
|
345
|
+
return states
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _element_texkey(
|
|
350
|
+
stack: PackStack,
|
|
351
|
+
ns: str,
|
|
352
|
+
face: dict[str, Any],
|
|
353
|
+
textures: dict[str, str],
|
|
354
|
+
block_id: str,
|
|
355
|
+
state_key: str,
|
|
356
|
+
texture_cache: dict[str, np.ndarray | None],
|
|
357
|
+
tex_arrays: dict[str, np.ndarray],
|
|
358
|
+
budget: _Budget,
|
|
359
|
+
) -> str | None:
|
|
360
|
+
"""Resolve one element face's texture ref into a tex array key."""
|
|
361
|
+
tex_ref = _resolve_ref(face.get("texture"), textures)
|
|
362
|
+
if tex_ref is None:
|
|
363
|
+
return None
|
|
364
|
+
return _ref_array_key(
|
|
365
|
+
stack,
|
|
366
|
+
ns,
|
|
367
|
+
tex_ref,
|
|
368
|
+
"tintindex" in face,
|
|
369
|
+
block_id,
|
|
370
|
+
state_key,
|
|
371
|
+
texture_cache,
|
|
372
|
+
tex_arrays,
|
|
373
|
+
budget,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _ref_array_key(
|
|
379
|
+
stack: PackStack,
|
|
380
|
+
ns: str,
|
|
381
|
+
tex_ref: str,
|
|
382
|
+
tinted: bool,
|
|
383
|
+
block_id: str,
|
|
384
|
+
state_key: str,
|
|
385
|
+
texture_cache: dict[str, np.ndarray | None],
|
|
386
|
+
tex_arrays: dict[str, np.ndarray],
|
|
387
|
+
budget: _Budget,
|
|
388
|
+
) -> str | None:
|
|
389
|
+
"""Load a texture ref and register its raw or tinted array key."""
|
|
390
|
+
tex_key = _texture_key(tex_ref, ns)
|
|
391
|
+
raw = _load_texture(stack, ns, tex_ref, tex_key, texture_cache, budget)
|
|
392
|
+
if raw is None:
|
|
393
|
+
return None
|
|
394
|
+
tint = _tint_for(block_id, state_key) if tinted else None
|
|
395
|
+
return _register_array(raw, tex_key, tint, tex_arrays)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _stub_quads(
|
|
400
|
+
stack: PackStack,
|
|
401
|
+
ns: str,
|
|
402
|
+
model: dict[str, Any],
|
|
403
|
+
block_id: str,
|
|
404
|
+
state_key: str,
|
|
405
|
+
texture_cache: dict[str, np.ndarray | None],
|
|
406
|
+
tex_arrays: dict[str, np.ndarray],
|
|
407
|
+
budget: _Budget,
|
|
408
|
+
) -> np.ndarray | None:
|
|
409
|
+
"""Build unit-cube quads from an element-less model's first texture."""
|
|
410
|
+
array_key = _ref_array_key(
|
|
411
|
+
stack,
|
|
412
|
+
ns,
|
|
413
|
+
model["stub_ref"],
|
|
414
|
+
True,
|
|
415
|
+
block_id,
|
|
416
|
+
state_key,
|
|
417
|
+
texture_cache,
|
|
418
|
+
tex_arrays,
|
|
419
|
+
budget,
|
|
420
|
+
)
|
|
421
|
+
if array_key is None:
|
|
422
|
+
return None
|
|
423
|
+
return unit_cube_quads(array_key)
|