litematica-render 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {litematica_render-0.1.0 → litematica_render-0.1.1}/PKG-INFO +5 -2
- {litematica_render-0.1.0 → litematica_render-0.1.1}/README.md +4 -1
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/__init__.py +1 -1
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/__init__.py +46 -11
- litematica_render-0.1.1/src/litematic/render/entity.py +384 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/pack/extract.py +203 -3
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/pack/loader.py +24 -3
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/pack/resolve.py +10 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/quads.py +11 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/scene/isometric.py +215 -115
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/scene/painter.py +217 -90
- litematica_render-0.1.1/src/litematic/render/scene/view.py +87 -0
- litematica_render-0.1.1/tests/render/entity/test_entity_frames.py +186 -0
- litematica_render-0.1.1/tests/render/entity/test_entity_items.py +106 -0
- litematica_render-0.1.1/tests/render/entity/test_entity_stands.py +89 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/pack/test_pack_extract.py +101 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/pack/test_pack_load.py +78 -2
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/scene/test_compose.py +188 -24
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/scene/test_paint.py +58 -36
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/scene/test_sizing.py +4 -4
- litematica_render-0.1.1/tests/render/scene/test_view.py +104 -0
- litematica_render-0.1.1/tests/render/shapes/__init__.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/test_api.py +42 -4
- {litematica_render-0.1.0 → litematica_render-0.1.1}/.github/workflows/python-publish.yml +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/.github/workflows/test.yml +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/.gitignore +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/LICENSE +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/pyproject.toml +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/parse/__init__.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/parse/errors.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/parse/model.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/parse/nbt.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/parse/parser.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/errors.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/pack/__init__.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/pack/blockstate.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/pack/compile.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/pack/elements.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/pack/entities.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/pack/stack.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/pack/texture.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/png.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/scene/__init__.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/shapes/__init__.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/shapes/boxes.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/src/litematic/render/shapes/families.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/helpers/__init__.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/helpers/litematica_packer.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/helpers/nbt_writer.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/parse/__init__.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/parse/test_api.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/parse/test_errors.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/parse/test_model.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/parse/test_nbt.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/parse/test_parser.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/parse/test_robustness.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/parse/test_slab.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/parse/test_unpack.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/__init__.py +0 -0
- {litematica_render-0.1.0/tests/render/pack → litematica_render-0.1.1/tests/render/entity}/__init__.py +0 -0
- {litematica_render-0.1.0/tests/render/scene → litematica_render-0.1.1/tests/render/pack}/__init__.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/pack/test_pack_entities.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/pack/test_pack_stack.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/pack/test_resolve.py +0 -0
- {litematica_render-0.1.0/tests/render/shapes → litematica_render-0.1.1/tests/render/scene}/__init__.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/shapes/test_shapes.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/test_init.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/test_png.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/test_quads.py +0 -0
- {litematica_render-0.1.0 → litematica_render-0.1.1}/tests/render/test_unresolved.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: litematica-render
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Parse Minecraft Litematica schematics and render them to isometric PNGs
|
|
5
5
|
Project-URL: Homepage, https://github.com/late-maple/Litematica-Render
|
|
6
6
|
Author: late-maple
|
|
@@ -27,7 +27,8 @@ Offline isometric rendering of Minecraft Litematica `.litematic` schematics.
|
|
|
27
27
|
## Features
|
|
28
28
|
|
|
29
29
|
- Parse `.litematic` files: streaming NBT, lazy vectorized region unpacking
|
|
30
|
-
- Render
|
|
30
|
+
- Render PNGs from a resource pack's real blockstates, models, and textures
|
|
31
|
+
- Free camera: any rotation/slant angle with per-pixel depth buffering
|
|
31
32
|
- Pack stack: layer add-on packs (e.g. Faithful 32x) over a vanilla base, MC-style first-hit lookup
|
|
32
33
|
|
|
33
34
|
## Example
|
|
@@ -42,6 +43,8 @@ png = isometric_png(
|
|
|
42
43
|
schem,
|
|
43
44
|
textures_path="vanilla-assets.zip", # base pack, required
|
|
44
45
|
extra_packs=["Faithful 32x.zip"], # add-ons, earlier = higher priority
|
|
46
|
+
rotation=160.0, # camera azimuth degrees (default 45)
|
|
47
|
+
slant=25.0, # camera elevation in [-90, 90]
|
|
45
48
|
max_size=2048,
|
|
46
49
|
)
|
|
47
50
|
with open("render.png", "wb") as f:
|
|
@@ -5,7 +5,8 @@ Offline isometric rendering of Minecraft Litematica `.litematic` schematics.
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- Parse `.litematic` files: streaming NBT, lazy vectorized region unpacking
|
|
8
|
-
- Render
|
|
8
|
+
- Render PNGs from a resource pack's real blockstates, models, and textures
|
|
9
|
+
- Free camera: any rotation/slant angle with per-pixel depth buffering
|
|
9
10
|
- Pack stack: layer add-on packs (e.g. Faithful 32x) over a vanilla base, MC-style first-hit lookup
|
|
10
11
|
|
|
11
12
|
## Example
|
|
@@ -20,6 +21,8 @@ png = isometric_png(
|
|
|
20
21
|
schem,
|
|
21
22
|
textures_path="vanilla-assets.zip", # base pack, required
|
|
22
23
|
extra_packs=["Faithful 32x.zip"], # add-ons, earlier = higher priority
|
|
24
|
+
rotation=160.0, # camera azimuth degrees (default 45)
|
|
25
|
+
slant=25.0, # camera elevation in [-90, 90]
|
|
23
26
|
max_size=2048,
|
|
24
27
|
)
|
|
25
28
|
with open("render.png", "wb") as f:
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"""Offline isometric rendering of parsed .litematic schematics."""
|
|
2
2
|
|
|
3
|
-
from collections.abc import Sequence
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
8
8
|
from litematic.parse.model import Litematic
|
|
9
|
+
from litematic.render.entity import item_content_texkey
|
|
9
10
|
from litematic.render.errors import RenderError
|
|
10
11
|
from litematic.render.pack import load_pack
|
|
11
12
|
from litematic.render.png import encode_png
|
|
12
13
|
from litematic.render.scene.isometric import _AIR_NAMES, _compose
|
|
13
14
|
from litematic.render.scene.painter import _choose_scale, _downsample, _paint
|
|
15
|
+
from litematic.render.scene.view import ISO_SLANT_DEG, view_axes
|
|
14
16
|
|
|
15
17
|
__all__ = [
|
|
16
18
|
"RenderError",
|
|
@@ -29,14 +31,14 @@ def isometric(
|
|
|
29
31
|
*,
|
|
30
32
|
textures_path: str | Path | None = None,
|
|
31
33
|
extra_packs: Sequence[str | Path] = (),
|
|
32
|
-
|
|
34
|
+
entities: bool = True,
|
|
35
|
+
rotation: float = 45.0,
|
|
36
|
+
slant: float = ISO_SLANT_DEG,
|
|
33
37
|
max_size: int = 2048,
|
|
34
38
|
supersample: int = 2,
|
|
35
39
|
background: tuple[int, int, int, int] | None = None,
|
|
36
40
|
) -> np.ndarray:
|
|
37
41
|
"""Render a schematic to an RGBA isometric image within max_size."""
|
|
38
|
-
if yaw not in (0, 1, 2, 3):
|
|
39
|
-
raise RenderError(f"render: yaw must be 0-3, got {yaw}")
|
|
40
42
|
if not _MAX_SIZE_BOUNDS[0] <= max_size <= _MAX_SIZE_BOUNDS[1]:
|
|
41
43
|
raise RenderError(
|
|
42
44
|
f"render: max_size must be "
|
|
@@ -44,12 +46,16 @@ def isometric(
|
|
|
44
46
|
)
|
|
45
47
|
if not _SS_BOUNDS[0] <= supersample <= _SS_BOUNDS[1]:
|
|
46
48
|
raise RenderError(
|
|
47
|
-
f"render: supersample must be
|
|
48
|
-
f"got {supersample}"
|
|
49
|
+
f"render: supersample must be "
|
|
50
|
+
f"{_SS_BOUNDS[0]}-{_SS_BOUNDS[1]}, got {supersample}"
|
|
49
51
|
)
|
|
52
|
+
if not isinstance(entities, bool):
|
|
53
|
+
raise RenderError(f"render: entities must be a boolean, got {entities!r}")
|
|
54
|
+
# Validate angles before any pack work so bad views fail fast.
|
|
55
|
+
view_axes(rotation, slant)
|
|
50
56
|
bg = _background(background)
|
|
51
57
|
textures = load_pack(_require_pack(textures_path), extra_packs)
|
|
52
|
-
scene = _compose(schem,
|
|
58
|
+
scene = _compose(schem, rotation, slant, textures, entities)
|
|
53
59
|
if len(scene.x) == 0:
|
|
54
60
|
raise RenderError("render: no blocks to render")
|
|
55
61
|
t, _ = _choose_scale(scene.span_w, scene.span_h, max_size, supersample)
|
|
@@ -68,7 +74,9 @@ def isometric_png(
|
|
|
68
74
|
*,
|
|
69
75
|
textures_path: str | Path | None = None,
|
|
70
76
|
extra_packs: Sequence[str | Path] = (),
|
|
71
|
-
|
|
77
|
+
entities: bool = True,
|
|
78
|
+
rotation: float = 45.0,
|
|
79
|
+
slant: float = ISO_SLANT_DEG,
|
|
72
80
|
max_size: int = 2048,
|
|
73
81
|
supersample: int = 2,
|
|
74
82
|
background: tuple[int, int, int, int] | None = None,
|
|
@@ -79,7 +87,9 @@ def isometric_png(
|
|
|
79
87
|
schem,
|
|
80
88
|
textures_path=textures_path,
|
|
81
89
|
extra_packs=extra_packs,
|
|
82
|
-
|
|
90
|
+
entities=entities,
|
|
91
|
+
rotation=rotation,
|
|
92
|
+
slant=slant,
|
|
83
93
|
max_size=max_size,
|
|
84
94
|
supersample=supersample,
|
|
85
95
|
background=background,
|
|
@@ -97,16 +107,25 @@ def unresolved_colors(
|
|
|
97
107
|
*,
|
|
98
108
|
textures_path: str | Path,
|
|
99
109
|
extra_packs: Sequence[str | Path] = (),
|
|
110
|
+
entities: bool = True,
|
|
100
111
|
) -> set[str]:
|
|
101
|
-
"""Return
|
|
112
|
+
"""Return names the pack stack cannot cover. Palette blocks and frame
|
|
113
|
+
content render magenta; dropped items and armor stands skip silently
|
|
114
|
+
but are still reported. With entities=False only palette names are
|
|
115
|
+
checked."""
|
|
116
|
+
if not isinstance(entities, bool):
|
|
117
|
+
raise RenderError(f"render: entities must be a boolean, got {entities!r}")
|
|
102
118
|
textures = load_pack(_require_pack(textures_path), extra_packs)
|
|
103
|
-
|
|
119
|
+
missed = {
|
|
104
120
|
state.name
|
|
105
121
|
for region in schem.regions.values()
|
|
106
122
|
for state in region.palette
|
|
107
123
|
if state.name not in _AIR_NAMES
|
|
108
124
|
and textures.resolve(state).miss is not None
|
|
109
125
|
}
|
|
126
|
+
if entities:
|
|
127
|
+
missed |= _unresolved_entities(schem, textures)
|
|
128
|
+
return missed
|
|
110
129
|
|
|
111
130
|
|
|
112
131
|
def _background(
|
|
@@ -132,3 +151,19 @@ def _require_pack(textures_path: str | Path | None) -> str | Path:
|
|
|
132
151
|
"render: a resource pack zip is required (pass textures_path)"
|
|
133
152
|
)
|
|
134
153
|
return textures_path
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _unresolved_entities(schem: Litematic, textures) -> set[str]:
|
|
157
|
+
"""Item ids the entity builders cannot draw; skipped or magenta."""
|
|
158
|
+
missed: set[str] = set()
|
|
159
|
+
for region in schem.regions.values():
|
|
160
|
+
for ent in region.entities:
|
|
161
|
+
stem = ent.id.rpartition(":")[2]
|
|
162
|
+
if stem not in ("item", "item_frame", "glow_item_frame"):
|
|
163
|
+
continue
|
|
164
|
+
item = ent.data.get("Item")
|
|
165
|
+
item_id = item.get("id") if isinstance(item, Mapping) else None
|
|
166
|
+
if isinstance(item_id, str) and item_id:
|
|
167
|
+
if item_content_texkey(item_id, textures) is None:
|
|
168
|
+
missed.add(item_id)
|
|
169
|
+
return missed
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Entities compiled to quads: dropped items, item frames, armor stands."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from litematic.render.pack.resolve import _MAGENTA_TEXKEY, FRAME_PAD_KEY
|
|
8
|
+
from litematic.render.quads import CULL_NONE, QUAD_DTYPE, TIER_SOUTH
|
|
9
|
+
|
|
10
|
+
# Vanilla ground display, frozen at the bob mid-point: generated items
|
|
11
|
+
# scale 0.5 (half-size sprite), block items 0.25 (mini cube), both hover
|
|
12
|
+
# 0.1 + 1/16 - bbox.minY above the floor (ItemEntityRenderer static pose).
|
|
13
|
+
ITEM_FLAT_SIZE = 8.0
|
|
14
|
+
ITEM_BLOCK_SCALE = 0.25
|
|
15
|
+
ITEM_MINI_BASE = 2.6
|
|
16
|
+
|
|
17
|
+
# Facing yaw turns matching the compose convention. Vanilla puts the pad
|
|
18
|
+
# on the wall OPPOSITE the facing (createBoundingBox: center.relative(
|
|
19
|
+
# facing, -0.46875)), the front looking along the facing.
|
|
20
|
+
_FACING_TURNS = {"south": 2, "north": 0, "east": 3, "west": 1}
|
|
21
|
+
# Vanilla Facing NBT byte -> facing name (up/down unsupported in v1).
|
|
22
|
+
_NBT_FACING = {0: "down", 1: "up", 2: "north", 3: "south", 4: "west", 5: "east"}
|
|
23
|
+
_ARMORSTAND_TEXKEY = "entity/armorstand/armorstand"
|
|
24
|
+
|
|
25
|
+
# Face tiers after one 90-degree turn (south -> east), matching compose.
|
|
26
|
+
_TIER_TURNS = {
|
|
27
|
+
1: np.array([0, 1, 4, 5, 3, 2], np.uint8),
|
|
28
|
+
2: np.array([0, 1, 3, 2, 5, 4], np.uint8),
|
|
29
|
+
3: np.array([0, 1, 5, 4, 2, 3], np.uint8),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def item_entity_quads(item_id: str, textures) -> np.ndarray | None:
|
|
35
|
+
"""Cell-local quads for one dropped item, or None when unrenderable."""
|
|
36
|
+
entry = _item_entry(item_id, textures)
|
|
37
|
+
if entry is None:
|
|
38
|
+
return None
|
|
39
|
+
if "block" in entry:
|
|
40
|
+
return _mini_block_quads(entry["block"], textures)
|
|
41
|
+
return _flat_item_quads(entry["flat"], textures)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def magenta_entity_quads() -> np.ndarray:
|
|
46
|
+
"""Fallback sprite for unresolvable frame content, in the frame geometry."""
|
|
47
|
+
return _one_quad(_MAGENTA_TEXKEY, _frame_corners())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def entity_props(data: object) -> dict[str, object]:
|
|
52
|
+
"""Normalize raw entity NBT into the properties builders consume."""
|
|
53
|
+
src: Mapping[str, object] = data if isinstance(data, Mapping) else {}
|
|
54
|
+
key = src.get("Facing")
|
|
55
|
+
props: dict[str, object] = {
|
|
56
|
+
"facing": _NBT_FACING.get(key) if isinstance(key, int) else None
|
|
57
|
+
}
|
|
58
|
+
rotation = src.get("Rotation")
|
|
59
|
+
if isinstance(rotation, (list, tuple)) and rotation:
|
|
60
|
+
yaw = _as_float(rotation[0])
|
|
61
|
+
if yaw is not None:
|
|
62
|
+
props["yaw"] = yaw
|
|
63
|
+
for flag in ("Invisible", "Small", "NoBasePlate", "ShowArms"):
|
|
64
|
+
if flag in src:
|
|
65
|
+
props[flag] = bool(src[flag])
|
|
66
|
+
turns = _as_int(src.get("ItemRotation", 0))
|
|
67
|
+
props["item_rotation"] = 0 if turns is None else turns
|
|
68
|
+
return props
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _as_float(value: object) -> float | None:
|
|
72
|
+
if isinstance(value, bool):
|
|
73
|
+
return None
|
|
74
|
+
if isinstance(value, (int, float)):
|
|
75
|
+
return float(value)
|
|
76
|
+
if isinstance(value, str):
|
|
77
|
+
try:
|
|
78
|
+
return float(value)
|
|
79
|
+
except ValueError:
|
|
80
|
+
return None
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _as_int(value: object) -> int | None:
|
|
85
|
+
number = _as_float(value)
|
|
86
|
+
return None if number is None else int(number)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def item_frame_quads(
|
|
91
|
+
properties: dict[str, object],
|
|
92
|
+
item_id: str | None,
|
|
93
|
+
textures,
|
|
94
|
+
) -> np.ndarray | None:
|
|
95
|
+
"""Wall-frame pad plus content quads, or None when nothing renders."""
|
|
96
|
+
facing = properties.get("facing")
|
|
97
|
+
if facing not in _FACING_TURNS:
|
|
98
|
+
return None
|
|
99
|
+
turns = _FACING_TURNS[facing]
|
|
100
|
+
pad = (
|
|
101
|
+
None
|
|
102
|
+
if properties.get("Invisible")
|
|
103
|
+
else _frame_pad_quads(facing, textures)
|
|
104
|
+
)
|
|
105
|
+
content = _frame_content_quads(properties, item_id, textures)
|
|
106
|
+
if content is not None:
|
|
107
|
+
content = _turn_cell_quads(content, turns)
|
|
108
|
+
if pad is None and content is None:
|
|
109
|
+
return None
|
|
110
|
+
return np.concatenate([q for q in (pad, content) if q is not None])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def armor_stand_quads(properties: dict[str, object], textures) -> np.ndarray | None:
|
|
115
|
+
"""Box-group quads for one armor stand, or None when unrenderable."""
|
|
116
|
+
if properties.get("Invisible"):
|
|
117
|
+
return None
|
|
118
|
+
if _ARMORSTAND_TEXKEY not in textures.arrays:
|
|
119
|
+
return None
|
|
120
|
+
small = bool(properties.get("Small"))
|
|
121
|
+
groups = [
|
|
122
|
+
_part_quads(uv, box, size, pivot, flag, small)
|
|
123
|
+
for name, uv, box, size, pivot, flag in _ARMOR_STAND_PARTS
|
|
124
|
+
if not (flag == "arms" and not properties.get("ShowArms"))
|
|
125
|
+
and not (flag == "plate" and properties.get("NoBasePlate"))
|
|
126
|
+
]
|
|
127
|
+
quads = np.concatenate(groups)
|
|
128
|
+
yaw = _as_float(properties.get("yaw"))
|
|
129
|
+
if yaw:
|
|
130
|
+
angle = np.deg2rad(yaw)
|
|
131
|
+
cos, sin = np.float32(np.cos(angle)), np.float32(np.sin(angle))
|
|
132
|
+
vtx = quads["vtx"]
|
|
133
|
+
dx, dz = vtx[..., 0] - 8.0, vtx[..., 2] - 8.0
|
|
134
|
+
vtx[..., 0] = 8.0 + dx * cos - dz * sin
|
|
135
|
+
vtx[..., 2] = 8.0 + dx * sin + dz * cos
|
|
136
|
+
return quads
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _item_entry(item_id: str, textures) -> dict[str, str] | None:
|
|
141
|
+
"""Display entry for an item, or a direct sprite when the pack has one."""
|
|
142
|
+
entry = textures.item_display.get(item_id)
|
|
143
|
+
if entry is not None:
|
|
144
|
+
return entry
|
|
145
|
+
direct = f"item/{item_id.rpartition(':')[2]}"
|
|
146
|
+
if direct in textures.arrays:
|
|
147
|
+
return {"flat": direct}
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _mini_block_quads(block_id: str, textures) -> np.ndarray | None:
|
|
153
|
+
quads = textures.pack_quads(block_id)
|
|
154
|
+
if quads is None or len(quads) == 0:
|
|
155
|
+
return None
|
|
156
|
+
out = quads.copy()
|
|
157
|
+
vtx = out["vtx"]
|
|
158
|
+
vtx[..., 0] = 8.0 - 8.0 * ITEM_BLOCK_SCALE + vtx[..., 0] * ITEM_BLOCK_SCALE
|
|
159
|
+
vtx[..., 1] = ITEM_MINI_BASE + vtx[..., 1] * ITEM_BLOCK_SCALE
|
|
160
|
+
vtx[..., 2] = 8.0 - 8.0 * ITEM_BLOCK_SCALE + vtx[..., 2] * ITEM_BLOCK_SCALE
|
|
161
|
+
out["dbl"] = True
|
|
162
|
+
out["cull"] = CULL_NONE
|
|
163
|
+
return out
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _flat_item_quads(texkey: str, textures) -> np.ndarray | None:
|
|
168
|
+
if texkey != _MAGENTA_TEXKEY and texkey not in textures.arrays:
|
|
169
|
+
return None
|
|
170
|
+
return _one_quad(texkey, _flat_corners())
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _flat_corners() -> np.ndarray:
|
|
175
|
+
half = ITEM_FLAT_SIZE / 2
|
|
176
|
+
top = ITEM_MINI_BASE + ITEM_FLAT_SIZE
|
|
177
|
+
return np.array(
|
|
178
|
+
[
|
|
179
|
+
[8.0 - half, top, 8.0],
|
|
180
|
+
[8.0 + half, top, 8.0],
|
|
181
|
+
[8.0 + half, ITEM_MINI_BASE, 8.0],
|
|
182
|
+
[8.0 - half, ITEM_MINI_BASE, 8.0],
|
|
183
|
+
],
|
|
184
|
+
np.float32,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _one_quad(texkey: str, corners: np.ndarray) -> np.ndarray:
|
|
190
|
+
quads = np.zeros(1, QUAD_DTYPE)
|
|
191
|
+
quads["vtx"][0] = corners
|
|
192
|
+
quads["uv"][0] = ((0, 0), (16, 0), (16, 16), (0, 16))
|
|
193
|
+
quads["texkey"][0] = texkey
|
|
194
|
+
quads["shade"][0] = TIER_SOUTH
|
|
195
|
+
quads["dbl"][0] = True
|
|
196
|
+
quads["cull"][0] = CULL_NONE
|
|
197
|
+
return quads
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _frame_pad_quads(facing: str, textures) -> np.ndarray | None:
|
|
202
|
+
pad = textures.mquad_map.get(FRAME_PAD_KEY)
|
|
203
|
+
if pad is not None and len(pad):
|
|
204
|
+
return _turn_cell_quads(pad, _FACING_TURNS[facing])
|
|
205
|
+
# Texture-only packs: the frame sprite stands in for the missing model.
|
|
206
|
+
if "item/item_frame" not in textures.arrays:
|
|
207
|
+
return None
|
|
208
|
+
quad = _one_quad(
|
|
209
|
+
"item/item_frame",
|
|
210
|
+
np.array(
|
|
211
|
+
[
|
|
212
|
+
[2.0, 14.0, 15.5],
|
|
213
|
+
[14.0, 14.0, 15.5],
|
|
214
|
+
[14.0, 2.0, 15.5],
|
|
215
|
+
[2.0, 2.0, 15.5],
|
|
216
|
+
],
|
|
217
|
+
np.float32,
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
quad["uv"][0] = ((2, 2), (14, 2), (14, 14), (2, 14))
|
|
221
|
+
return _turn_cell_quads(quad, _FACING_TURNS[facing])
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def item_content_texkey(item_id: str, textures) -> str | None:
|
|
226
|
+
"""Flat texkey drawn for an item, or None when nothing can render it."""
|
|
227
|
+
entry = _item_entry(item_id, textures)
|
|
228
|
+
if entry is None:
|
|
229
|
+
return None
|
|
230
|
+
if "flat" in entry:
|
|
231
|
+
return entry["flat"] if entry["flat"] in textures.arrays else None
|
|
232
|
+
plate = textures.pack_quads(entry["block"])
|
|
233
|
+
if plate is None or len(plate) == 0:
|
|
234
|
+
return None
|
|
235
|
+
return str(plate["texkey"][0])
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _frame_content_quads(
|
|
240
|
+
properties: dict[str, object],
|
|
241
|
+
item_id: str | None,
|
|
242
|
+
textures,
|
|
243
|
+
) -> np.ndarray | None:
|
|
244
|
+
"""Half-size sprite centered on the pad, flush with its front plane."""
|
|
245
|
+
if item_id is None:
|
|
246
|
+
return None
|
|
247
|
+
texkey = item_content_texkey(item_id, textures)
|
|
248
|
+
sprite = (
|
|
249
|
+
magenta_entity_quads()
|
|
250
|
+
if texkey is None
|
|
251
|
+
else _one_quad(texkey, _frame_corners())
|
|
252
|
+
)
|
|
253
|
+
return _turn_content(sprite, _as_int(properties.get("item_rotation", 0)) or 0)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _frame_corners() -> np.ndarray:
|
|
258
|
+
"""Content plane per vanilla FIXED: sprite mid at the pad front (z=15),
|
|
259
|
+
half-size sprite centered on the pad center."""
|
|
260
|
+
half = ITEM_FLAT_SIZE / 2
|
|
261
|
+
return np.array(
|
|
262
|
+
[
|
|
263
|
+
[8.0 - half, 8.0 + half, 15.0],
|
|
264
|
+
[8.0 + half, 8.0 + half, 15.0],
|
|
265
|
+
[8.0 + half, 8.0 - half, 15.0],
|
|
266
|
+
[8.0 - half, 8.0 - half, 15.0],
|
|
267
|
+
],
|
|
268
|
+
np.float32,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _turn_content(sprite: np.ndarray, turns: int) -> np.ndarray:
|
|
274
|
+
"""Spin the pad-plane sprite in 45-degree item steps about the center."""
|
|
275
|
+
out = sprite.copy()
|
|
276
|
+
if turns:
|
|
277
|
+
angle = np.deg2rad(45.0 * turns)
|
|
278
|
+
cos, sin = np.float32(np.cos(angle)), np.float32(np.sin(angle))
|
|
279
|
+
vtx = out["vtx"]
|
|
280
|
+
dx, dy = vtx[..., 0] - 8.0, vtx[..., 1] - 8.0
|
|
281
|
+
vtx[..., 0] = 8.0 + dx * cos - dy * sin
|
|
282
|
+
vtx[..., 1] = 8.0 + dx * sin + dy * cos
|
|
283
|
+
out["cull"] = CULL_NONE
|
|
284
|
+
out["dbl"] = True
|
|
285
|
+
return out
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _turn_cell_quads(quads: np.ndarray, turns: int) -> np.ndarray:
|
|
290
|
+
"""Rotate cell-local quads about the Y axis, compose turn convention."""
|
|
291
|
+
turns %= 4
|
|
292
|
+
if turns == 0:
|
|
293
|
+
return quads
|
|
294
|
+
out = quads.copy()
|
|
295
|
+
vtx = out["vtx"]
|
|
296
|
+
dx, dz = vtx[..., 0] - 8.0, vtx[..., 2] - 8.0
|
|
297
|
+
if turns == 1:
|
|
298
|
+
nx, nz = dz, -dx
|
|
299
|
+
elif turns == 2:
|
|
300
|
+
nx, nz = -dx, -dz
|
|
301
|
+
else:
|
|
302
|
+
nx, nz = -dz, dx
|
|
303
|
+
vtx[..., 0] = 8.0 + nx
|
|
304
|
+
vtx[..., 2] = 8.0 + nz
|
|
305
|
+
out["shade"] = _TIER_TURNS[turns][out["shade"]]
|
|
306
|
+
return out
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# Vanilla ArmorStandModel boxes (64x64 texture space, model y-down, 24 =
|
|
311
|
+
# ground): (uv, box offset, size, pivot, flag). World mapping: x/z as-is,
|
|
312
|
+
# y = 24 - model_y; small applies the BabyModelTransform pivot/box scaling.
|
|
313
|
+
_ARMOR_STAND_PARTS = (
|
|
314
|
+
("head", (0, 0), (-1, -7, -1), (2, 7, 2), (0, 1, 0), "head"),
|
|
315
|
+
("body", (0, 26), (-6, 0, -1.5), (12, 3, 3), (0, 0, 0), ""),
|
|
316
|
+
("right_body_stick", (16, 0), (-3, 3, -1), (2, 7, 2), (0, 0, 0), ""),
|
|
317
|
+
("left_body_stick", (48, 16), (1, 3, -1), (2, 7, 2), (0, 0, 0), ""),
|
|
318
|
+
("shoulder_stick", (0, 48), (-4, 10, -1), (8, 2, 2), (0, 0, 0), ""),
|
|
319
|
+
("right_arm", (24, 0), (-2, -2, -1), (2, 12, 2), (-5, 2, 0), "arms"),
|
|
320
|
+
("left_arm", (32, 16), (0, -2, -1), (2, 12, 2), (5, 2, 0), "arms"),
|
|
321
|
+
("right_leg", (8, 0), (-1, 0, -1), (2, 11, 2), (-1.9, 12, 0), ""),
|
|
322
|
+
("left_leg", (40, 16), (-1, 0, -1), (2, 11, 2), (1.9, 12, 0), ""),
|
|
323
|
+
("base_plate", (0, 32), (-6, 11, -6), (12, 1, 12), (0, 12, 0), "plate"),
|
|
324
|
+
)
|
|
325
|
+
_PART_FACES = (
|
|
326
|
+
("down", 0),
|
|
327
|
+
("up", 1),
|
|
328
|
+
("north", 2),
|
|
329
|
+
("south", 3),
|
|
330
|
+
("west", 4),
|
|
331
|
+
("east", 5),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _part_quads(
|
|
337
|
+
uv: tuple[int, int],
|
|
338
|
+
box: tuple[float, float, float],
|
|
339
|
+
size: tuple[float, float, float],
|
|
340
|
+
pivot: tuple[float, float, float],
|
|
341
|
+
flag: str,
|
|
342
|
+
small: bool,
|
|
343
|
+
) -> np.ndarray:
|
|
344
|
+
scale, lift = (0.75, 16.0) if flag == "head" and small else (1.0, 0.0)
|
|
345
|
+
if small and flag != "head":
|
|
346
|
+
scale, lift = 0.5, 24.0
|
|
347
|
+
px, py, pz = (
|
|
348
|
+
pivot[0] * scale,
|
|
349
|
+
(pivot[1] + lift) * scale,
|
|
350
|
+
pivot[2] * scale,
|
|
351
|
+
)
|
|
352
|
+
lo = np.array(box, np.float32) * scale
|
|
353
|
+
hi = lo + np.array(size, np.float32) * scale
|
|
354
|
+
x0, x1 = 8.0 + px + lo[0], 8.0 + px + hi[0]
|
|
355
|
+
z0, z1 = 8.0 + pz + lo[2], 8.0 + pz + hi[2]
|
|
356
|
+
y0, y1 = 24.0 - (py + hi[1]), 24.0 - (py + lo[1])
|
|
357
|
+
u, v = uv
|
|
358
|
+
dz, dx, dy = size[2] * scale, size[0] * scale, size[1] * scale
|
|
359
|
+
rects = {
|
|
360
|
+
"up": (u + dz, v, u + dz + dx, v + dz),
|
|
361
|
+
"down": (u + dz + dx, v, u + dz + 2 * dx, v + dz),
|
|
362
|
+
"west": (u, v + dz, u + dz, v + dz + dy),
|
|
363
|
+
"north": (u + dz, v + dz, u + dz + dx, v + dz + dy),
|
|
364
|
+
"east": (u + dz + dx, v + dz, u + dz + dx + dz, v + dz + dy),
|
|
365
|
+
"south": (u + dz + dx + dz, v + dz, u + dz + dx + dz + dx, v + dz + dy),
|
|
366
|
+
}
|
|
367
|
+
corners = {
|
|
368
|
+
"down": ((x0, y0, z1), (x1, y0, z1), (x1, y0, z0), (x0, y0, z0)),
|
|
369
|
+
"up": ((x0, y1, z0), (x1, y1, z0), (x1, y1, z1), (x0, y1, z1)),
|
|
370
|
+
"north": ((x1, y1, z0), (x0, y1, z0), (x0, y0, z0), (x1, y0, z0)),
|
|
371
|
+
"south": ((x0, y1, z1), (x1, y1, z1), (x1, y0, z1), (x0, y0, z1)),
|
|
372
|
+
"west": ((x0, y1, z1), (x0, y1, z0), (x0, y0, z0), (x0, y0, z1)),
|
|
373
|
+
"east": ((x1, y1, z0), (x1, y1, z1), (x1, y0, z1), (x1, y0, z0)),
|
|
374
|
+
}
|
|
375
|
+
quads = np.zeros(6, QUAD_DTYPE)
|
|
376
|
+
for i, (face, tier) in enumerate(_PART_FACES):
|
|
377
|
+
fu0, fv0, fu1, fv1 = (c / 4.0 for c in rects[face])
|
|
378
|
+
quads["vtx"][i] = corners[face]
|
|
379
|
+
quads["uv"][i] = ((fu0, fv0), (fu1, fv0), (fu1, fv1), (fu0, fv1))
|
|
380
|
+
quads["texkey"][i] = _ARMORSTAND_TEXKEY
|
|
381
|
+
quads["shade"][i] = tier
|
|
382
|
+
quads["dbl"][i] = True
|
|
383
|
+
quads["cull"][i] = CULL_NONE
|
|
384
|
+
return quads
|