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.
@@ -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)