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,421 @@
|
|
|
1
|
+
"""Model element geometry: boxes compiled to textured quads."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import Any, Callable, cast
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from litematic.render.pack.texture import _rotate
|
|
9
|
+
from litematic.render.quads import (
|
|
10
|
+
_CUBE_VTX,
|
|
11
|
+
CULL_NONE,
|
|
12
|
+
QUAD_DTYPE,
|
|
13
|
+
TIER_DOWN,
|
|
14
|
+
TIER_EAST,
|
|
15
|
+
TIER_NORTH,
|
|
16
|
+
TIER_SOUTH,
|
|
17
|
+
TIER_UP,
|
|
18
|
+
TIER_WEST,
|
|
19
|
+
is_parallelogram,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_OPAQUE_ALPHA = 250.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_FACE_TIER = {
|
|
27
|
+
"down": TIER_DOWN,
|
|
28
|
+
"up": TIER_UP,
|
|
29
|
+
"north": TIER_NORTH,
|
|
30
|
+
"south": TIER_SOUTH,
|
|
31
|
+
"west": TIER_WEST,
|
|
32
|
+
"east": TIER_EAST,
|
|
33
|
+
}
|
|
34
|
+
# Auto-UVs follow vanilla defaultFaceUV; ring order (u1,v1),(u2,v1),(u2,v2),(u1,v2).
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_VEC = {
|
|
39
|
+
"up": (0, 1, 0),
|
|
40
|
+
"down": (0, -1, 0),
|
|
41
|
+
"north": (0, 0, -1),
|
|
42
|
+
"south": (0, 0, 1),
|
|
43
|
+
"west": (-1, 0, 0),
|
|
44
|
+
"east": (1, 0, 0),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_ROT_AXIS = {"x": 0, "y": 1, "z": 2}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_RESCALE_FACTORS = {22.5: 1.0823922, 45.0: 1.4142135}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_FACE_GEOM: dict[int, tuple[tuple[int, int, int], ...]] = {
|
|
58
|
+
tier: tuple((int(c[0]), int(c[1]), int(c[2])) for c in corners)
|
|
59
|
+
for tier, corners in enumerate(_CUBE_VTX)
|
|
60
|
+
}
|
|
61
|
+
# Vanilla element rotation: 90-degree-free angles about one axis through an
|
|
62
|
+
# origin, optionally rescaled on the perpendicular axes by 1/cos(|angle|).
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _corner_tuple(point: Any) -> tuple[float, float, float]:
|
|
67
|
+
"""Convert one vertex row to a plain three-float tuple."""
|
|
68
|
+
x, y, z = (float(value) for value in point)
|
|
69
|
+
return (x, y, z)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _sorted_corners(quad: Any) -> list[tuple[float, float, float]]:
|
|
74
|
+
"""Return a quad's corners as a sorted, order-independent list."""
|
|
75
|
+
return sorted(_corner_tuple(point) for point in quad["vtx"])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_UNIT_CORNERS: dict[int, list[tuple[float, float, float]]] = {
|
|
79
|
+
tier: sorted(_corner_tuple(corner) for corner in corners)
|
|
80
|
+
for tier, corners in _FACE_GEOM.items()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Unit-cube corners per tier as sorted order-independent lists: blockstate
|
|
86
|
+
# rotations map a face onto its own square with reversed winding, so an
|
|
87
|
+
# element-wise comparison would wrongly reject rotated opaque cubes.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _unit_cube_occl(
|
|
93
|
+
quads: np.ndarray, tex_arrays: dict[str, np.ndarray]
|
|
94
|
+
) -> bool:
|
|
95
|
+
"""Judge the v1 opaque-cube rule: exact unit cube, visible faces opaque."""
|
|
96
|
+
if len(quads) != 6:
|
|
97
|
+
return False
|
|
98
|
+
by_tier: dict[int, Any] = {}
|
|
99
|
+
for quad in quads:
|
|
100
|
+
by_tier[int(quad["shade"])] = quad
|
|
101
|
+
if set(by_tier) != set(_FACE_GEOM):
|
|
102
|
+
return False
|
|
103
|
+
for tier in _FACE_GEOM:
|
|
104
|
+
if _sorted_corners(by_tier[tier]) != _UNIT_CORNERS[tier]:
|
|
105
|
+
return False
|
|
106
|
+
alphas: list[float] = []
|
|
107
|
+
for tier in (TIER_UP, TIER_SOUTH, TIER_EAST):
|
|
108
|
+
array = tex_arrays.get(str(by_tier[tier]["texkey"]))
|
|
109
|
+
if array is None:
|
|
110
|
+
return False
|
|
111
|
+
alphas.append(float(array[..., 3].mean()))
|
|
112
|
+
return float(np.mean(alphas)) >= _OPAQUE_ALPHA
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _compile_elements(
|
|
117
|
+
elements: list[dict[str, Any]],
|
|
118
|
+
textures: dict[str, str],
|
|
119
|
+
ns: str,
|
|
120
|
+
x_rot: int,
|
|
121
|
+
y_rot: int,
|
|
122
|
+
texkey_for: Callable[[dict[str, Any], dict[str, str], str], str | None],
|
|
123
|
+
*,
|
|
124
|
+
uvlock: bool = False,
|
|
125
|
+
) -> np.ndarray | None:
|
|
126
|
+
"""Compile model elements into world-space quads for one variant."""
|
|
127
|
+
rows: list[tuple[Any, Any, str, int, bool, int]] = []
|
|
128
|
+
kx = (int(x_rot) // 90) % 4
|
|
129
|
+
ky = (int(y_rot) // 90) % 4
|
|
130
|
+
for element in elements:
|
|
131
|
+
if not isinstance(element, dict):
|
|
132
|
+
continue
|
|
133
|
+
bounds = _element_bounds(element)
|
|
134
|
+
faces = element.get("faces")
|
|
135
|
+
if bounds is None or not isinstance(faces, dict):
|
|
136
|
+
continue
|
|
137
|
+
try:
|
|
138
|
+
rotation = _element_rotation(element)
|
|
139
|
+
except ValueError:
|
|
140
|
+
continue
|
|
141
|
+
thin = _thin_axes(bounds)
|
|
142
|
+
if len(thin) > 1:
|
|
143
|
+
continue
|
|
144
|
+
rotated = rotation is not None
|
|
145
|
+
# Zero-thickness planes and rotated elements have non-axis-aligned
|
|
146
|
+
# or paired back faces: spec marks them dbl (double-sided, no AO).
|
|
147
|
+
double: bool = bool(thin) or rotated
|
|
148
|
+
shaded = bool(element.get("shade", True))
|
|
149
|
+
for face_name, tier in _FACE_TIER.items():
|
|
150
|
+
face = faces.get(face_name)
|
|
151
|
+
if not isinstance(face, dict):
|
|
152
|
+
continue
|
|
153
|
+
if thin and _VEC[face_name][thin[0]] == 0:
|
|
154
|
+
continue
|
|
155
|
+
texkey = texkey_for(face, textures, ns)
|
|
156
|
+
if texkey is None:
|
|
157
|
+
continue
|
|
158
|
+
corners = _box_corners(bounds, tier)
|
|
159
|
+
uvs = _face_uvs(face, tier, corners)
|
|
160
|
+
if uvs is None:
|
|
161
|
+
continue
|
|
162
|
+
if double or not shaded:
|
|
163
|
+
shade: int = TIER_UP
|
|
164
|
+
else:
|
|
165
|
+
shade = _FACE_TIER[_rotate(face_name, x_rot, y_rot)]
|
|
166
|
+
if rotation is not None:
|
|
167
|
+
axis, angle, origin, factor = rotation
|
|
168
|
+
corners = [
|
|
169
|
+
_rotate_element_point(corner, axis, angle, origin, factor)
|
|
170
|
+
for corner in corners
|
|
171
|
+
]
|
|
172
|
+
cullface = face.get("cullface")
|
|
173
|
+
# A non-string cullface drops the face's cull, not the block.
|
|
174
|
+
cull = (
|
|
175
|
+
_FACE_TIER[_rotate(cullface, x_rot, y_rot)]
|
|
176
|
+
if isinstance(cullface, str) and cullface in _VEC
|
|
177
|
+
else CULL_NONE
|
|
178
|
+
)
|
|
179
|
+
# Vanilla bakes UV compensation only for uvlock variants.
|
|
180
|
+
turns = _UV_BAKE[(kx, ky)][tier] if uvlock else 0
|
|
181
|
+
uvs = [_bake_vertex_uv(uv, turns) for uv in uvs]
|
|
182
|
+
rows.append(
|
|
183
|
+
(_rotate_corners(corners, kx, ky), uvs, texkey, shade, double, cull)
|
|
184
|
+
)
|
|
185
|
+
if not rows:
|
|
186
|
+
return None
|
|
187
|
+
quads = np.empty(len(rows), dtype=QUAD_DTYPE)
|
|
188
|
+
for index, (corners, uvs, texkey, shade, dbl, cull) in enumerate(rows):
|
|
189
|
+
quads[index] = (corners, uvs, texkey, shade, dbl, cull)
|
|
190
|
+
keep = is_parallelogram(quads)
|
|
191
|
+
if not keep.any():
|
|
192
|
+
return None
|
|
193
|
+
return quads[keep]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _thin_axes(
|
|
198
|
+
bounds: tuple[float, float, float, float, float, float],
|
|
199
|
+
) -> list[int]:
|
|
200
|
+
"""Indices of the axes where from equals to (zero-thickness planes)."""
|
|
201
|
+
x0, y0, z0, x1, y1, z1 = bounds
|
|
202
|
+
sizes = (x1 - x0, y1 - y0, z1 - z0)
|
|
203
|
+
return [axis for axis, size in enumerate(sizes) if size == 0.0]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _element_rotation(
|
|
208
|
+
element: dict[str, Any],
|
|
209
|
+
) -> tuple[int, float, tuple[float, float, float], float] | None:
|
|
210
|
+
"""Parse rotation into (axis, angle, origin, rescale factor) or None."""
|
|
211
|
+
rot = element.get("rotation")
|
|
212
|
+
if rot is None:
|
|
213
|
+
return None
|
|
214
|
+
if not isinstance(rot, dict):
|
|
215
|
+
raise ValueError("element rotation must be a mapping")
|
|
216
|
+
axis = _ROT_AXIS.get(rot.get("axis", "y"))
|
|
217
|
+
if axis is None:
|
|
218
|
+
raise ValueError("element rotation has an unknown axis")
|
|
219
|
+
angle = rot.get("angle", 0.0)
|
|
220
|
+
if isinstance(angle, bool) or not isinstance(angle, (int, float)):
|
|
221
|
+
raise ValueError("element rotation angle must be a number")
|
|
222
|
+
angle = float(angle)
|
|
223
|
+
if not -45.0 <= angle <= 45.0 or angle % 22.5 != 0.0:
|
|
224
|
+
raise ValueError("element rotation angle out of the vanilla set")
|
|
225
|
+
origin = rot.get("origin", (8.0, 8.0, 8.0))
|
|
226
|
+
if not isinstance(origin, (list, tuple)) or len(origin) != 3:
|
|
227
|
+
raise ValueError("element rotation origin must be three numbers")
|
|
228
|
+
try:
|
|
229
|
+
point = tuple(float(value) for value in origin)
|
|
230
|
+
except (TypeError, ValueError, OverflowError) as exc:
|
|
231
|
+
raise ValueError("element rotation origin must be three numbers") from exc
|
|
232
|
+
if not all(math.isfinite(value) for value in point):
|
|
233
|
+
raise ValueError("element rotation origin must be finite")
|
|
234
|
+
rescale = bool(rot.get("rescale", False))
|
|
235
|
+
factor = _RESCALE_FACTORS.get(abs(angle), 1.0) if rescale else 1.0
|
|
236
|
+
return (axis, angle, cast("tuple[float, float, float]", point), factor)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _rotate_element_point(
|
|
241
|
+
point: tuple[float, float, float],
|
|
242
|
+
axis: int,
|
|
243
|
+
angle: float,
|
|
244
|
+
origin: tuple[float, float, float],
|
|
245
|
+
factor: float,
|
|
246
|
+
) -> tuple[float, float, float]:
|
|
247
|
+
"""Rotate one point by angle about the axis through origin, rescaled."""
|
|
248
|
+
rel = [point[i] - origin[i] for i in range(3)]
|
|
249
|
+
rad = math.radians(angle)
|
|
250
|
+
cos, sin = math.cos(rad), math.sin(rad)
|
|
251
|
+
if axis == 0:
|
|
252
|
+
rel[1], rel[2] = rel[1] * cos - rel[2] * sin, rel[1] * sin + rel[2] * cos
|
|
253
|
+
elif axis == 1:
|
|
254
|
+
rel[0], rel[2] = rel[0] * cos + rel[2] * sin, -rel[0] * sin + rel[2] * cos
|
|
255
|
+
else:
|
|
256
|
+
rel[0], rel[1] = rel[0] * cos - rel[1] * sin, rel[0] * sin + rel[1] * cos
|
|
257
|
+
if factor != 1.0:
|
|
258
|
+
for i in range(3):
|
|
259
|
+
if i != axis:
|
|
260
|
+
rel[i] *= factor
|
|
261
|
+
return (rel[0] + origin[0], rel[1] + origin[1], rel[2] + origin[2])
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _element_bounds(
|
|
266
|
+
element: dict[str, Any],
|
|
267
|
+
) -> tuple[float, float, float, float, float, float] | None:
|
|
268
|
+
"""Parse from/to into six finite floats, or None when malformed."""
|
|
269
|
+
frm = element.get("from")
|
|
270
|
+
to = element.get("to")
|
|
271
|
+
if not isinstance(frm, (list, tuple)) or not isinstance(to, (list, tuple)):
|
|
272
|
+
return None
|
|
273
|
+
if len(frm) != 3 or len(to) != 3:
|
|
274
|
+
return None
|
|
275
|
+
try:
|
|
276
|
+
values = tuple(float(value) for value in (*frm, *to))
|
|
277
|
+
except (TypeError, ValueError, OverflowError):
|
|
278
|
+
return None
|
|
279
|
+
if not all(math.isfinite(value) for value in values):
|
|
280
|
+
return None
|
|
281
|
+
return cast("tuple[float, float, float, float, float, float]", values)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _box_corners(
|
|
286
|
+
bounds: tuple[float, float, float, float, float, float], tier: int
|
|
287
|
+
) -> list[tuple[float, float, float]]:
|
|
288
|
+
"""Scale the pinned unit-cube corners of one tier onto an element box."""
|
|
289
|
+
x0, y0, z0, x1, y1, z1 = bounds
|
|
290
|
+
axis = ((x0, x1), (y0, y1), (z0, z1))
|
|
291
|
+
return [
|
|
292
|
+
(axis[0][corner[0] // 16], axis[1][corner[1] // 16], axis[2][corner[2] // 16])
|
|
293
|
+
for corner in _FACE_GEOM[tier]
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _face_uvs(
|
|
299
|
+
face: dict[str, Any], tier: int, corners: list[tuple[float, float, float]]
|
|
300
|
+
) -> list[tuple[float, float]] | None:
|
|
301
|
+
"""Resolve per-corner texel coords per the vanilla vertex tables."""
|
|
302
|
+
rect = face.get("uv")
|
|
303
|
+
if rect is None:
|
|
304
|
+
rect = _default_face_uv(tier, corners)
|
|
305
|
+
else:
|
|
306
|
+
if not isinstance(rect, (list, tuple)) or len(rect) != 4:
|
|
307
|
+
return None
|
|
308
|
+
try:
|
|
309
|
+
rect = (float(rect[0]), float(rect[1]), float(rect[2]), float(rect[3]))
|
|
310
|
+
except (TypeError, ValueError, OverflowError):
|
|
311
|
+
return None
|
|
312
|
+
if not all(math.isfinite(value) for value in rect):
|
|
313
|
+
return None
|
|
314
|
+
if min(rect) < 0.0 or max(rect) > 16.0:
|
|
315
|
+
return None
|
|
316
|
+
rotation = face.get("rotation")
|
|
317
|
+
shift = 0
|
|
318
|
+
if rotation is not None:
|
|
319
|
+
if isinstance(rotation, bool) or not isinstance(rotation, (int, float)):
|
|
320
|
+
return None
|
|
321
|
+
if rotation not in (0, 90, 180, 270):
|
|
322
|
+
return None
|
|
323
|
+
shift = int(rotation) // 90
|
|
324
|
+
u1, v1, u2, v2 = rect
|
|
325
|
+
corner_uv = ((u1, v1), (u1, v2), (u2, v2), (u2, v1))
|
|
326
|
+
return [
|
|
327
|
+
corner_uv[(vertex + shift) % 4]
|
|
328
|
+
for vertex in _VANILLA_VERTEX[tier]
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
_VANILLA_VERTEX = {
|
|
334
|
+
TIER_DOWN: (0, 3, 2, 1),
|
|
335
|
+
TIER_UP: (0, 3, 2, 1),
|
|
336
|
+
TIER_NORTH: (0, 3, 2, 1),
|
|
337
|
+
TIER_SOUTH: (0, 3, 2, 1),
|
|
338
|
+
TIER_WEST: (3, 0, 1, 2),
|
|
339
|
+
TIER_EAST: (3, 0, 1, 2),
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
_UV_BAKE = {
|
|
345
|
+
(0, 0): (0, 0, 0, 0, 0, 0),
|
|
346
|
+
(0, 1): (3, 1, 0, 0, 0, 0),
|
|
347
|
+
(0, 2): (2, 2, 0, 0, 0, 0),
|
|
348
|
+
(0, 3): (1, 3, 0, 0, 0, 0),
|
|
349
|
+
(1, 0): (0, 2, 2, 0, 3, 1),
|
|
350
|
+
(1, 1): (0, 2, 1, 1, 3, 1),
|
|
351
|
+
(1, 2): (0, 2, 0, 2, 3, 1),
|
|
352
|
+
(1, 3): (0, 2, 3, 3, 3, 1),
|
|
353
|
+
(2, 0): (0, 0, 2, 2, 2, 2),
|
|
354
|
+
(2, 1): (3, 1, 2, 2, 2, 2),
|
|
355
|
+
(2, 2): (2, 2, 2, 2, 2, 2),
|
|
356
|
+
(2, 3): (1, 3, 2, 2, 2, 2),
|
|
357
|
+
(3, 0): (2, 0, 2, 0, 1, 3),
|
|
358
|
+
(3, 1): (2, 0, 3, 3, 1, 3),
|
|
359
|
+
(3, 2): (2, 0, 0, 2, 1, 3),
|
|
360
|
+
(3, 3): (2, 0, 1, 1, 1, 3),
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _bake_vertex_uv(uv: tuple[float, float], turns: int) -> tuple[float, float]:
|
|
365
|
+
"""Rotate one texel coord around the tile center by quarter-turns."""
|
|
366
|
+
if turns == 0:
|
|
367
|
+
return uv
|
|
368
|
+
u, v = uv
|
|
369
|
+
if turns == 1:
|
|
370
|
+
return (16.0 - v, u)
|
|
371
|
+
if turns == 2:
|
|
372
|
+
return (16.0 - u, 16.0 - v)
|
|
373
|
+
return (v, 16.0 - u)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _default_face_uv(
|
|
377
|
+
tier: int, corners: list[tuple[float, float, float]]
|
|
378
|
+
) -> tuple[float, float, float, float]:
|
|
379
|
+
"""Vanilla defaultFaceUV extents for one tier from the element corners."""
|
|
380
|
+
xs = [corner[0] for corner in corners]
|
|
381
|
+
ys = [corner[1] for corner in corners]
|
|
382
|
+
zs = [corner[2] for corner in corners]
|
|
383
|
+
x0, x1 = min(xs), max(xs)
|
|
384
|
+
y0, y1 = min(ys), max(ys)
|
|
385
|
+
z0, z1 = min(zs), max(zs)
|
|
386
|
+
if tier == TIER_DOWN:
|
|
387
|
+
return (x0, 16.0 - z1, x1, 16.0 - z0)
|
|
388
|
+
if tier == TIER_UP:
|
|
389
|
+
return (x0, z0, x1, z1)
|
|
390
|
+
if tier == TIER_NORTH:
|
|
391
|
+
return (16.0 - x1, 16.0 - y1, 16.0 - x0, 16.0 - y0)
|
|
392
|
+
if tier == TIER_SOUTH:
|
|
393
|
+
return (x0, 16.0 - y1, x1, 16.0 - y0)
|
|
394
|
+
if tier == TIER_WEST:
|
|
395
|
+
return (z0, 16.0 - y1, z1, 16.0 - y0)
|
|
396
|
+
return (16.0 - z1, 16.0 - y1, 16.0 - z0, 16.0 - y0)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _rot_x_point(point: tuple[float, float, float]) -> tuple[float, float, float]:
|
|
401
|
+
"""Rotate one block-space point 90 degrees around the x axis."""
|
|
402
|
+
return (point[0], point[2], 16.0 - point[1])
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _rot_y_point(point: tuple[float, float, float]) -> tuple[float, float, float]:
|
|
407
|
+
"""Rotate one block-space point 90 degrees around the y axis."""
|
|
408
|
+
return (16.0 - point[2], point[1], point[0])
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _rotate_corners(
|
|
413
|
+
corners: list[tuple[float, float, float]], kx: int, ky: int
|
|
414
|
+
) -> list[tuple[float, float, float]]:
|
|
415
|
+
"""Apply x then y blockstate rotation steps to all four corners."""
|
|
416
|
+
out = corners
|
|
417
|
+
for _ in range(kx):
|
|
418
|
+
out = [_rot_x_point(point) for point in out]
|
|
419
|
+
for _ in range(ky):
|
|
420
|
+
out = [_rot_y_point(point) for point in out]
|
|
421
|
+
return out
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Block-entity-shaped blocks: chests compiled from entity textures."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from litematic.render.pack.blockstate import _state_pairs
|
|
6
|
+
from litematic.render.quads import (
|
|
7
|
+
CULL_NONE,
|
|
8
|
+
QUAD_DTYPE,
|
|
9
|
+
TIER_DOWN,
|
|
10
|
+
TIER_EAST,
|
|
11
|
+
TIER_NORTH,
|
|
12
|
+
TIER_SOUTH,
|
|
13
|
+
TIER_UP,
|
|
14
|
+
TIER_WEST,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_CHEST_BASES = {
|
|
18
|
+
"chest": "normal",
|
|
19
|
+
"trapped_chest": "trapped",
|
|
20
|
+
"ender_chest": "ender",
|
|
21
|
+
"copper_chest": "copper",
|
|
22
|
+
"waxed_copper_chest": "copper",
|
|
23
|
+
"exposed_copper_chest": "copper_exposed",
|
|
24
|
+
"waxed_exposed_copper_chest": "copper_exposed",
|
|
25
|
+
"weathered_copper_chest": "copper_weathered",
|
|
26
|
+
"waxed_weathered_copper_chest": "copper_weathered",
|
|
27
|
+
"oxidized_copper_chest": "copper_oxidized",
|
|
28
|
+
"waxed_oxidized_copper_chest": "copper_oxidized",
|
|
29
|
+
}
|
|
30
|
+
# The model's front is +Z (south); the rotation turns it to the facing.
|
|
31
|
+
_FACING_ROTATION = {"south": 0, "east": 90, "north": 180, "west": 270}
|
|
32
|
+
_COS_SIN = {0: (1, 0), 90: (0, 1), 180: (-1, 0), 270: (0, -1)}
|
|
33
|
+
|
|
34
|
+
_TIER_OF_NORMAL = {
|
|
35
|
+
(0, 1, 0): TIER_UP,
|
|
36
|
+
(0, -1, 0): TIER_DOWN,
|
|
37
|
+
(0, 0, -1): TIER_NORTH,
|
|
38
|
+
(0, 0, 1): TIER_SOUTH,
|
|
39
|
+
(-1, 0, 0): TIER_WEST,
|
|
40
|
+
(1, 0, 0): TIER_EAST,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def chest_quads(
|
|
45
|
+
block_id: str,
|
|
46
|
+
state_key: str,
|
|
47
|
+
resolve_texture,
|
|
48
|
+
) -> np.ndarray | None:
|
|
49
|
+
"""Compile one closed chest from entity textures, or None if uncovered.
|
|
50
|
+
|
|
51
|
+
`resolve_texture` maps an entity texture ref (entity/chest/...) to a
|
|
52
|
+
registered texkey, returning None when the pack lacks the file.
|
|
53
|
+
"""
|
|
54
|
+
stem = block_id.partition(":")[2]
|
|
55
|
+
base = _CHEST_BASES.get(stem)
|
|
56
|
+
if base is None:
|
|
57
|
+
return None
|
|
58
|
+
properties = _state_pairs(state_key)
|
|
59
|
+
kind = properties.get("type", "single")
|
|
60
|
+
if kind not in ("single", "left", "right"):
|
|
61
|
+
return None
|
|
62
|
+
texkey = resolve_texture(_chest_texture_ref(base, kind))
|
|
63
|
+
if texkey is None:
|
|
64
|
+
return None
|
|
65
|
+
degrees = _FACING_ROTATION.get(properties.get("facing", "north"), 180)
|
|
66
|
+
|
|
67
|
+
faces: list[tuple[list, list, tuple[int, int, int], str]] = []
|
|
68
|
+
if kind == "single":
|
|
69
|
+
_add_box(faces, (1, 0, 1), (15, 10, 15), (0, 19), texkey)
|
|
70
|
+
_add_box(faces, (1, 9, 1), (15, 14, 15), (0, 0), texkey)
|
|
71
|
+
_add_box(faces, (7, 7, 15), (9, 11, 16), (0, 0), texkey)
|
|
72
|
+
else:
|
|
73
|
+
x1, x2 = (0, 15) if kind == "left" else (1, 16)
|
|
74
|
+
_add_box(faces, (x1, 0, 1), (x2, 10, 15), (0, 19), texkey)
|
|
75
|
+
_add_box(faces, (x1, 9, 1), (x2, 14, 15), (0, 0), texkey)
|
|
76
|
+
lock_x = 0 if kind == "left" else 15
|
|
77
|
+
_add_box(faces, (lock_x, 7, 15), (lock_x + 1, 11, 16), (0, 0), texkey)
|
|
78
|
+
|
|
79
|
+
quads = np.empty(len(faces), dtype=QUAD_DTYPE)
|
|
80
|
+
for index, (vertices, uvs, normal, face_texkey) in enumerate(faces):
|
|
81
|
+
rotated = [_rotate_y(point, degrees) for point in vertices]
|
|
82
|
+
quads[index] = (
|
|
83
|
+
rotated,
|
|
84
|
+
[(u / 4.0, v / 4.0) for (u, v) in uvs],
|
|
85
|
+
face_texkey,
|
|
86
|
+
_TIER_OF_NORMAL[_rotate_normal(normal, degrees)],
|
|
87
|
+
False,
|
|
88
|
+
CULL_NONE,
|
|
89
|
+
)
|
|
90
|
+
return quads
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _chest_texture_ref(base: str, kind: str) -> str:
|
|
94
|
+
if kind in ("left", "right"):
|
|
95
|
+
return f"entity/chest/{base}_{kind}"
|
|
96
|
+
return f"entity/chest/{base}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _add_box(
|
|
100
|
+
faces: list,
|
|
101
|
+
frm: tuple[int, int, int],
|
|
102
|
+
to: tuple[int, int, int],
|
|
103
|
+
origin: tuple[int, int],
|
|
104
|
+
texkey: str,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Append one box's six faces in entity-unwrap order and UV layout."""
|
|
107
|
+
x1, y1, z1 = frm
|
|
108
|
+
x2, y2, z2 = to
|
|
109
|
+
width, height, depth = x2 - x1, y2 - y1, z2 - z1
|
|
110
|
+
u, v = origin
|
|
111
|
+
x1f, y1f, z1f, x2f, y2f, z2f = (
|
|
112
|
+
float(x1), float(y1), float(z1), float(x2), float(y2), float(z2)
|
|
113
|
+
)
|
|
114
|
+
uf, vf, wf, hf, df = (
|
|
115
|
+
float(u), float(v), float(width), float(height), float(depth)
|
|
116
|
+
)
|
|
117
|
+
faces.extend([
|
|
118
|
+
(
|
|
119
|
+
[(x1f, y1f, z2f), (x1f, y1f, z1f), (x2f, y1f, z1f), (x2f, y1f, z2f)],
|
|
120
|
+
[(uf + df, vf), (uf + df, vf + df),
|
|
121
|
+
(uf + df + wf, vf + df), (uf + df + wf, vf)],
|
|
122
|
+
(0, -1, 0),
|
|
123
|
+
texkey,
|
|
124
|
+
),
|
|
125
|
+
(
|
|
126
|
+
[(x1f, y2f, z1f), (x1f, y2f, z2f), (x2f, y2f, z2f), (x2f, y2f, z1f)],
|
|
127
|
+
[(uf + df + wf, vf + df), (uf + df + wf, vf),
|
|
128
|
+
(uf + df + 2 * wf, vf), (uf + df + 2 * wf, vf + df)],
|
|
129
|
+
(0, 1, 0),
|
|
130
|
+
texkey,
|
|
131
|
+
),
|
|
132
|
+
(
|
|
133
|
+
[(x1f, y1f, z1f), (x1f, y2f, z1f), (x2f, y2f, z1f), (x2f, y1f, z1f)],
|
|
134
|
+
[(uf + df, vf + df), (uf + df, vf + df + hf),
|
|
135
|
+
(uf + df + wf, vf + df + hf), (uf + df + wf, vf + df)],
|
|
136
|
+
(0, 0, -1),
|
|
137
|
+
texkey,
|
|
138
|
+
),
|
|
139
|
+
(
|
|
140
|
+
[(x2f, y1f, z2f), (x2f, y2f, z2f), (x1f, y2f, z2f), (x1f, y1f, z2f)],
|
|
141
|
+
[(uf + df + wf + df, vf + df), (uf + df + wf + df, vf + df + hf),
|
|
142
|
+
(uf + df + 2 * wf + df, vf + df + hf),
|
|
143
|
+
(uf + df + 2 * wf + df, vf + df)],
|
|
144
|
+
(0, 0, 1),
|
|
145
|
+
texkey,
|
|
146
|
+
),
|
|
147
|
+
(
|
|
148
|
+
[(x1f, y1f, z2f), (x1f, y2f, z2f), (x1f, y2f, z1f), (x1f, y1f, z1f)],
|
|
149
|
+
[(uf, vf + df), (uf, vf + df + hf),
|
|
150
|
+
(uf + df, vf + df + hf), (uf + df, vf + df)],
|
|
151
|
+
(-1, 0, 0),
|
|
152
|
+
texkey,
|
|
153
|
+
),
|
|
154
|
+
(
|
|
155
|
+
[(x2f, y1f, z1f), (x2f, y2f, z1f), (x2f, y2f, z2f), (x2f, y1f, z2f)],
|
|
156
|
+
[(uf + df + wf, vf + df), (uf + df + wf, vf + df + hf),
|
|
157
|
+
(uf + df + wf + df, vf + df + hf), (uf + df + wf + df, vf + df)],
|
|
158
|
+
(1, 0, 0),
|
|
159
|
+
texkey,
|
|
160
|
+
),
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _rotate_y(
|
|
165
|
+
point: tuple[float, float, float], degrees: int
|
|
166
|
+
) -> tuple[float, float, float]:
|
|
167
|
+
"""Rotate one block-space point about the y axis through (8, *, 8)."""
|
|
168
|
+
cos, sin = _COS_SIN[degrees]
|
|
169
|
+
x, y, z = point[0] - 8.0, point[1], point[2] - 8.0
|
|
170
|
+
return (x * cos + z * sin + 8.0, y, -x * sin + z * cos + 8.0)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _rotate_normal(
|
|
174
|
+
normal: tuple[int, int, int], degrees: int
|
|
175
|
+
) -> tuple[int, int, int]:
|
|
176
|
+
cos, sin = _COS_SIN[degrees]
|
|
177
|
+
return (
|
|
178
|
+
round(normal[0] * cos + normal[2] * sin),
|
|
179
|
+
normal[1],
|
|
180
|
+
round(-normal[0] * sin + normal[2] * cos),
|
|
181
|
+
)
|