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,357 @@
1
+ """Builtin shape table: box families plus multi-box and cross plants."""
2
+
3
+ from collections.abc import Callable, Mapping
4
+
5
+ import numpy as np
6
+
7
+ from litematic.render.errors import RenderError
8
+ from litematic.render.quads import (
9
+ CULL_NONE,
10
+ QUAD_DTYPE,
11
+ TIER_EAST,
12
+ TIER_NORTH,
13
+ TIER_SOUTH,
14
+ TIER_UP,
15
+ TIER_WEST,
16
+ _redstone_tint,
17
+ )
18
+ from litematic.render.shapes.boxes import (
19
+ Box,
20
+ _banner,
21
+ _barrel,
22
+ _button,
23
+ _cactus,
24
+ _carpet,
25
+ _chest,
26
+ _combine,
27
+ _composter,
28
+ _conduit,
29
+ _door,
30
+ _facing,
31
+ _flag,
32
+ _fluid,
33
+ _head,
34
+ _ladder,
35
+ _lectern,
36
+ _lily_pad,
37
+ _pressure_plate,
38
+ _rail,
39
+ _repeater,
40
+ _shulker_box,
41
+ _sign,
42
+ _slab,
43
+ _snow,
44
+ _stairs,
45
+ _text,
46
+ _torch_standing,
47
+ _torch_wall,
48
+ _trapdoor,
49
+ box_quads,
50
+ )
51
+
52
+ __all__ = ["builtin_quads", "box_quads"]
53
+
54
+ Builder = Callable[[Mapping[str, str]], np.ndarray]
55
+ _SIDES = ("north", "south", "west", "east")
56
+
57
+ # Connection geometry per family (Blockscope shape.rs taxonomy): fences
58
+ # carry two rails per connected side at y 6..9 and 12..15, walls one full
59
+ # arm with the post height driven by "up" (low and tall arms share the
60
+ # same 0..14 box, so tall arms' cross-block reach is approximated), panes
61
+ # and iron bars one full arm per side plus a full bar when unconnected.
62
+ _FENCE_POST: Box = (6, 0, 6, 10, 16, 10)
63
+ _FENCE_ARMS: dict[str, tuple[Box, Box]] = {
64
+ "north": ((6, 6, 0, 10, 9, 6), (6, 12, 0, 10, 15, 6)),
65
+ "south": ((6, 6, 10, 10, 9, 16), (6, 12, 10, 10, 15, 16)),
66
+ "west": ((0, 6, 6, 6, 9, 10), (0, 12, 6, 6, 15, 10)),
67
+ "east": ((10, 6, 6, 16, 9, 10), (10, 12, 6, 16, 15, 10)),
68
+ }
69
+ _GATE_POST: Box = (6, 0, 6, 10, 16, 10)
70
+ # Open gate bars stay centered over the full block depth per Blockscope,
71
+ # coplanarly overlapping the post side faces; both faces share the same
72
+ # auto-UV texel mapping, so the overlap does not change rendered pixels.
73
+ _GATE_BAR_X: Box = (0, 6, 6, 16, 15, 10)
74
+ _GATE_BAR_Z: Box = (6, 6, 0, 10, 15, 16)
75
+ _WALL_ARMS: dict[str, Box] = {
76
+ "north": (4, 0, 0, 12, 14, 4),
77
+ "south": (4, 0, 12, 12, 14, 16),
78
+ "west": (0, 0, 4, 4, 14, 12),
79
+ "east": (12, 0, 4, 16, 14, 12),
80
+ }
81
+ _PANE_POST: Box = (7, 0, 7, 9, 16, 9)
82
+ _PANE_ARMS: dict[str, Box] = {
83
+ "north": (7, 0, 0, 9, 16, 7),
84
+ "south": (7, 0, 9, 9, 16, 16),
85
+ "west": (0, 0, 7, 7, 16, 9),
86
+ "east": (9, 0, 7, 16, 16, 9),
87
+ }
88
+ _PANE_BAR: Box = (7, 0, 0, 9, 16, 16)
89
+
90
+ # Cross plants: two diagonal planes spanning the full block height,
91
+ # texture v running bottom (v=16) to top (v=0); both quads are double-
92
+ # sided and carry the up tier like the vanilla cross model.
93
+ _CROSS_VTX = np.array(
94
+ [
95
+ [(0, 0, 0), (16, 0, 16), (16, 16, 16), (0, 16, 0)],
96
+ [(16, 0, 0), (0, 0, 16), (0, 16, 16), (16, 16, 0)],
97
+ ],
98
+ dtype="f4",
99
+ )
100
+ _CROSS_UV = np.tile(
101
+ np.array([(0, 16), (16, 16), (16, 0), (0, 0)], dtype="f4"), (2, 1, 1)
102
+ )
103
+
104
+ # Cross-plant block names rendered as two diagonal planes (Blockscope
105
+ # taxonomy plus the sapling/propagule suffix families).
106
+ _CROSS_PLANTS = frozenset(
107
+ {
108
+ "short_grass",
109
+ "tall_grass",
110
+ "fern",
111
+ "large_fern",
112
+ "dandelion",
113
+ "poppy",
114
+ "blue_orchid",
115
+ "allium",
116
+ "azure_bluet",
117
+ "oxeye_daisy",
118
+ "cornflower",
119
+ "lily_of_the_valley",
120
+ "red_tulip",
121
+ "orange_tulip",
122
+ "white_tulip",
123
+ "pink_tulip",
124
+ "sunflower",
125
+ "lilac",
126
+ "rose_bush",
127
+ "peony",
128
+ "vine",
129
+ "cobweb",
130
+ "bamboo",
131
+ "dead_bush",
132
+ "seagrass",
133
+ "kelp",
134
+ "kelp_plant",
135
+ "sugar_cane",
136
+ "wheat",
137
+ "carrots",
138
+ "potatoes",
139
+ "beetroots",
140
+ "torchflower",
141
+ "pitcher_plant",
142
+ "bush",
143
+ "sweet_berry_bush",
144
+ "tall_seagrass",
145
+ "red_mushroom",
146
+ "brown_mushroom",
147
+ "crimson_roots",
148
+ "warped_roots",
149
+ "nether_sprouts",
150
+ "crimson_fungus",
151
+ "warped_fungus",
152
+ }
153
+ )
154
+
155
+
156
+ def _fence(properties: Mapping[str, str]) -> np.ndarray:
157
+ """Fence: center post plus two rails on every connected side."""
158
+ boxes: list[Box] = [_FENCE_POST]
159
+ for side in _SIDES:
160
+ if _flag(properties, side):
161
+ boxes.extend(_FENCE_ARMS[side])
162
+ return _combine(boxes)
163
+
164
+
165
+ def _fence_gate(properties: Mapping[str, str]) -> np.ndarray:
166
+ """Fence gate: center post plus a bar swung aside when open."""
167
+ along_x = _facing(properties) in ("north", "south")
168
+ bar = _GATE_BAR_X if along_x else _GATE_BAR_Z
169
+ if _flag(properties, "open"):
170
+ bar = _GATE_BAR_Z if along_x else _GATE_BAR_X
171
+ return _combine([_GATE_POST, bar])
172
+
173
+
174
+ def _side(properties: Mapping[str, str], key: str) -> bool:
175
+ """True when a wall side property is present and not "none"."""
176
+ return _text(properties, key, "none") != "none"
177
+
178
+
179
+ def _wall(properties: Mapping[str, str]) -> np.ndarray:
180
+ """Wall: center column plus one arm per connected side."""
181
+ top = 16.0 if _flag(properties, "up") else 14.0
182
+ boxes: list[Box] = [(4, 0, 4, 12, top, 12)]
183
+ for side in _SIDES:
184
+ if _side(properties, side):
185
+ boxes.append(_WALL_ARMS[side])
186
+ return _combine(boxes)
187
+
188
+
189
+ def _pane(properties: Mapping[str, str]) -> np.ndarray:
190
+ """Pane and iron bars: center post plus arms, full bar when alone."""
191
+ boxes: list[Box] = [_PANE_POST]
192
+ connected = False
193
+ for side in _SIDES:
194
+ if _flag(properties, side):
195
+ boxes.append(_PANE_ARMS[side])
196
+ connected = True
197
+ if not connected:
198
+ boxes.append(_PANE_BAR)
199
+ return _combine(boxes)
200
+
201
+
202
+ def _cross(properties: Mapping[str, str]) -> np.ndarray:
203
+ """Cross plant: two diagonal double-sided planes at full height."""
204
+ quads = np.empty(2, dtype=QUAD_DTYPE)
205
+ quads["vtx"] = _CROSS_VTX
206
+ quads["uv"] = _CROSS_UV
207
+ quads["texkey"] = ""
208
+ quads["shade"] = TIER_UP
209
+ quads["dbl"] = True
210
+ quads["cull"] = CULL_NONE
211
+ return quads
212
+
213
+
214
+ def _named(*names: str) -> Callable[[str], bool]:
215
+ """Match one of the exact block names."""
216
+ return lambda stem: stem in names
217
+
218
+
219
+ def _suffixed(suffix: str) -> Callable[[str], bool]:
220
+ """Match any block name ending with the suffix."""
221
+ return lambda stem: stem.endswith(suffix)
222
+
223
+
224
+ def _is_shulker(stem: str) -> bool:
225
+ """Match the shulker box family including the base block."""
226
+ return stem == "shulker_box" or stem.endswith("_shulker_box")
227
+
228
+
229
+ def _is_head(stem: str) -> bool:
230
+ """Match heads and skulls."""
231
+ return stem.endswith("_head") or stem.endswith("skull")
232
+
233
+
234
+ def _is_wall_torch(stem: str) -> bool:
235
+ """Match wall-mounted torches."""
236
+ return stem == "wall_torch" or stem.endswith("_wall_torch")
237
+
238
+
239
+ def _is_torch(stem: str) -> bool:
240
+ """Match standing torches (checked after wall torches)."""
241
+ return stem == "torch" or stem.endswith("_torch")
242
+
243
+
244
+ def _is_pane(stem: str) -> bool:
245
+ """Match glass panes and the iron bars block."""
246
+ return stem == "iron_bars" or stem.endswith("_pane")
247
+
248
+
249
+ def _is_cross(stem: str) -> bool:
250
+ """Match cross-plant names and sapling/propagule suffixes."""
251
+ return stem in _CROSS_PLANTS or stem.endswith(("_sapling", "_propagule"))
252
+
253
+
254
+ def _ground_plane(transposed: bool, texkey: str) -> np.ndarray:
255
+ """One full-width dust plane on the block floor; transposed uv runs E-W."""
256
+ quad = box_quads(0, 0, 0, 16, 0, 16)[[TIER_UP]]
257
+ if transposed:
258
+ quad["uv"][0] = quad["vtx"][0][:, [2, 0]]
259
+ quad["texkey"][0] = texkey
260
+ return quad
261
+
262
+
263
+ def _wall_strip(tier: int, texkey: str) -> np.ndarray:
264
+ """One full-height dust strip on a block face, visible from both sides."""
265
+ bounds = {
266
+ TIER_NORTH: (0, 0, 0, 16, 16, 0),
267
+ TIER_SOUTH: (0, 0, 16, 16, 16, 16),
268
+ TIER_WEST: (0, 0, 0, 0, 16, 16),
269
+ TIER_EAST: (16, 0, 0, 16, 16, 16),
270
+ }[tier]
271
+ quad = box_quads(*bounds)[[tier]]
272
+ quad["texkey"][0] = texkey
273
+ quad["dbl"][0] = True
274
+ quad["shade"][0] = TIER_UP
275
+ return quad
276
+
277
+
278
+ def _wire(properties: Mapping[str, str]) -> np.ndarray:
279
+ """Redstone dust: ground planes per connected axis, wall strips for up."""
280
+ try:
281
+ power = max(0, min(15, int(properties.get("power", "0"))))
282
+ except (TypeError, ValueError):
283
+ power = 0
284
+ r, g, b = _redstone_tint(power)
285
+ line = f"block/redstone_dust_line0@{r},{g},{b}"
286
+ dot = f"block/redstone_dust_dot@{r},{g},{b}"
287
+ connected = [
288
+ properties.get(direction, "none") in ("side", "up")
289
+ for direction in _SIDES
290
+ ]
291
+ quads: list[np.ndarray] = []
292
+ if not any(connected):
293
+ quads.append(_ground_plane(False, dot))
294
+ else:
295
+ # North-south planes sample the texture column along x; east-west
296
+ # planes transpose the uv so the same column runs along z.
297
+ if connected[0] or connected[1]:
298
+ quads.append(_ground_plane(False, line))
299
+ if connected[2] or connected[3]:
300
+ quads.append(_ground_plane(True, line))
301
+ for direction, tier in zip(_SIDES, (TIER_NORTH, TIER_SOUTH, TIER_WEST, TIER_EAST)):
302
+ if properties.get(direction, "none") == "up":
303
+ quads.append(_wall_strip(tier, line))
304
+ return np.concatenate(quads)
305
+
306
+
307
+ # Ordered dispatch, earlier rows win: wall torches must precede standing
308
+ # torches because "*_wall_torch" also ends with "_torch". The other rows
309
+ # are pairwise disjoint and keep Blockscope's precedence for reference.
310
+ _FAMILIES: tuple[tuple[Callable[[str], bool], Builder], ...] = (
311
+ (_suffixed("_fence_gate"), _fence_gate),
312
+ (_suffixed("_fence"), _fence),
313
+ (_suffixed("_wall"), _wall),
314
+ (_is_pane, _pane),
315
+ (_is_cross, _cross),
316
+ (_named("water", "lava", "bubble_column"), _fluid),
317
+ (_named("snow"), _snow),
318
+ (_named("redstone_wire"), _wire),
319
+ (_named("composter"), _composter),
320
+ (_suffixed("_slab"), _slab),
321
+ (_suffixed("_stairs"), _stairs),
322
+ (_suffixed("_carpet"), _carpet),
323
+ (_suffixed("rail"), _rail),
324
+ (_suffixed("_pressure_plate"), _pressure_plate),
325
+ (_named("repeater", "comparator"), _repeater),
326
+ (_suffixed("_trapdoor"), _trapdoor),
327
+ (_suffixed("_door"), _door),
328
+ (_named("ladder"), _ladder),
329
+ (_suffixed("_button"), _button),
330
+ (_suffixed("sign"), _sign),
331
+ (_suffixed("banner"), _banner),
332
+ (_named("chest", "trapped_chest", "ender_chest"), _chest),
333
+ (_is_shulker, _shulker_box),
334
+ (_named("barrel"), _barrel),
335
+ (_is_head, _head),
336
+ (_named("cactus"), _cactus),
337
+ (_named("lily_pad"), _lily_pad),
338
+ (_named("conduit"), _conduit),
339
+ (_named("lectern"), _lectern),
340
+ (_is_wall_torch, _torch_wall),
341
+ (_is_torch, _torch_standing),
342
+ )
343
+
344
+
345
+ def builtin_quads(
346
+ state_name: str, properties: Mapping[str, str]
347
+ ) -> np.ndarray | None:
348
+ """Build geometry-only quads for a known family, else None."""
349
+ if not isinstance(state_name, str):
350
+ raise RenderError("shapes: block state name must be a string")
351
+ if not isinstance(properties, Mapping):
352
+ raise RenderError("shapes: block properties must be a mapping")
353
+ stem = state_name.partition(":")[2] or state_name
354
+ for matches, build in _FAMILIES:
355
+ if matches(stem):
356
+ return build(properties)
357
+ return None
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.5
2
+ Name: litematica-render
3
+ Version: 0.1.0
4
+ Summary: Parse Minecraft Litematica schematics and render them to isometric PNGs
5
+ Project-URL: Homepage, https://github.com/late-maple/Litematica-Render
6
+ Author: late-maple
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Games/Entertainment
11
+ Classifier: Topic :: Multimedia :: Graphics
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: numpy
14
+ Provides-Extra: dev
15
+ Requires-Dist: mypy; extra == 'dev'
16
+ Requires-Dist: pillow>=9.1; extra == 'dev'
17
+ Requires-Dist: pytest; extra == 'dev'
18
+ Requires-Dist: ruff; extra == 'dev'
19
+ Provides-Extra: pack
20
+ Requires-Dist: pillow>=9.1; extra == 'pack'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Litematica-Render
24
+
25
+ Offline isometric rendering of Minecraft Litematica `.litematic` schematics.
26
+
27
+ ## Features
28
+
29
+ - Parse `.litematic` files: streaming NBT, lazy vectorized region unpacking
30
+ - Render isometric PNGs from a resource pack's real blockstates, models, and textures
31
+ - Pack stack: layer add-on packs (e.g. Faithful 32x) over a vanilla base, MC-style first-hit lookup
32
+
33
+ ## Example
34
+
35
+ ```python
36
+ from litematic import load
37
+ from litematic.render import isometric_png, unresolved_colors
38
+
39
+ schem = load("schematic.litematic")
40
+
41
+ png = isometric_png(
42
+ schem,
43
+ textures_path="vanilla-assets.zip", # base pack, required
44
+ extra_packs=["Faithful 32x.zip"], # add-ons, earlier = higher priority
45
+ max_size=2048,
46
+ )
47
+ with open("render.png", "wb") as f:
48
+ f.write(png)
49
+
50
+ # block names the pack stack cannot cover; those render magenta
51
+ missing = unresolved_colors(schem, textures_path="vanilla-assets.zip")
52
+ ```
@@ -0,0 +1,30 @@
1
+ litematic/__init__.py,sha256=E2jYR3b82jQXR81PqeT4A01P6J1gvrVRu8RRZ0BcGfg,715
2
+ litematic/parse/__init__.py,sha256=l4mrsKjEn7snnwkUCX9olPWXjRcJGW0PX6s4RG3HfPE,70
3
+ litematic/parse/errors.py,sha256=Kx5GCUdpRJcHqjF8J-znSlGGCb4NHDbR2cQULzp-9Z8,390
4
+ litematic/parse/model.py,sha256=Tt4-atRpqYEGHRIQgDJ8oWABmqmN4vbKSpJen0i-8Q8,7212
5
+ litematic/parse/nbt.py,sha256=PkpyfGraPDpHQexXmdAMADi_7DHCDcOZr5upTihFTNU,5137
6
+ litematic/parse/parser.py,sha256=rHDgIlgiaRGF9ZlX3dmAuDH-b84ot5rtOOTUV1X0ZCE,11765
7
+ litematic/render/__init__.py,sha256=enttGyJ4q7To8tJUx9Liw6R92hqNQ8n_o1oL-gq3-qA,4038
8
+ litematic/render/errors.py,sha256=EY1MDCoRCXZ0YMOj1IodOUsNq56RJ3rpp4wBjszI-Ys,221
9
+ litematic/render/png.py,sha256=t9E28uSm_FysBCPmK7Tng927EP-pctPRsPR7NlfK6Rw,1451
10
+ litematic/render/quads.py,sha256=keEFJgM1JJFXSueenyQKQUepI6mK80dT4oAIEHWHWQM,3202
11
+ litematic/render/pack/__init__.py,sha256=P-4XDsFVJ2kVdr6o89n55x5tUhaWUEbHNPDR8LYm9Po,320
12
+ litematic/render/pack/blockstate.py,sha256=wozrHtq1SOWQ1BfWd0IUjbVl1GftL5b1TIVevCY3QG4,4021
13
+ litematic/render/pack/compile.py,sha256=_DR7SA4tk_jk0ZsnX3kGZN-jwGcysdGVzMu4sX-vAAs,12448
14
+ litematic/render/pack/elements.py,sha256=lMoZztRGGEKO1u8-eUiHctRnsAegAdgojsJwdJOd6zI,13645
15
+ litematic/render/pack/entities.py,sha256=SRDUJqlDmQPi88qSu9j5SP4lYgyD2sLDXaLtVNDdN8Y,6027
16
+ litematic/render/pack/extract.py,sha256=09UTQLSWZo2eX7zM1iy8x3r6n6QS_GWIkDtsd8UuEQA,8192
17
+ litematic/render/pack/loader.py,sha256=48Nw4RnurJQBg4V9Lzc_MiFEdRiLVm4K7pUgZ-twluI,6768
18
+ litematic/render/pack/resolve.py,sha256=BoyWX9ZQ7aqusYoLFHRYajrKhbKkJNKfZjkLYhA7p1o,11025
19
+ litematic/render/pack/stack.py,sha256=0sBboFXIYij9TZ3Of1dEGux72Eh1KBwHh0NcJCRx_ks,2876
20
+ litematic/render/pack/texture.py,sha256=yrvYRfhLaVlAoFBY6ruKTolHwXwb6dFfGhysWPSl-fs,10386
21
+ litematic/render/scene/__init__.py,sha256=DXSoN_mQMVw12MkWfY5RJF6bqXmqoc5b7J2gX1h5FuE,266
22
+ litematic/render/scene/isometric.py,sha256=Gs9MA8KxBB4jvzuOrKzvOLWFxFQWoAv4VkYcj7dnZPk,19832
23
+ litematic/render/scene/painter.py,sha256=b3pI4CgjSoXsfj6e5Si46UeShcliBIy45_bRoUgrlBE,14184
24
+ litematic/render/shapes/__init__.py,sha256=nbLcM_HO2l6U4uUMR7_5liMMuvexYdbz3ZHIxoTJlUw,232
25
+ litematic/render/shapes/boxes.py,sha256=v8GkWeo7xrr9bbWV2t-m1XM6IZpKfS3zQgXuLlRrRb4,9603
26
+ litematic/render/shapes/families.py,sha256=yzfN5GnUHSJC8sB3qmrhLVOgd2mGYfIjWd05Cx0ZGkY,11130
27
+ litematica_render-0.1.0.dist-info/METADATA,sha256=C-od2iPBWy4oASOpmn539uSv8_RHImIsB3vb-rwckfU,1648
28
+ litematica_render-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
29
+ litematica_render-0.1.0.dist-info/licenses/LICENSE,sha256=FeHjRX52Zw1w0SHoK2lW2xttM_HUYk_bsdFcoKPPTrk,1067
30
+ litematica_render-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 late_maple
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.