Normalize generated docs and asset naming

This commit is contained in:
2026-08-11 01:39:08 +01:00
parent bc10c0a0a1
commit 31ca76b922
859 changed files with 637 additions and 17132 deletions
+90 -57
View File
@@ -13,6 +13,18 @@ from typing import Any
from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageOps
PREVIEW_SCALE_MULTIPLIER = 1.25
PREVIEW_MARGIN_RATIO = 0.22
UV_SAMPLE_SCALE = 16
PREVIEW_MODEL_ROTATION_Y = 180
def load_json(path: Path) -> Any:
with path.open("r", encoding="utf-8") as handle:
return json.load(handle)
@@ -34,6 +46,57 @@ def texture_ref_to_path(pack_root: Path, texture_ref: str) -> Path:
return pack_root / "assets" / namespace / "textures" / f"{path}.png"
def first_animation_frame(image: Image.Image, texture_path: Path) -> Image.Image:
if getattr(image, "is_animated", False):
image.seek(0)
return image.copy()
mcmeta_path = texture_path.with_suffix(".png.mcmeta")
if not mcmeta_path.exists():
return image.copy()
try:
metadata = load_json(mcmeta_path)
except Exception:
return image.copy()
animation = metadata.get("animation") if isinstance(metadata, dict) else None
if not isinstance(animation, dict):
return image.copy()
frame_width = int(animation.get("width") or 0)
frame_height = int(animation.get("height") or 0)
if frame_width <= 0 or frame_height <= 0:
if image.height >= image.width and image.height % image.width == 0:
frame_width = frame_height = image.width
elif image.width > image.height and image.width % image.height == 0:
frame_width = frame_height = image.height
else:
frame_width, frame_height = image.size
frames = animation.get("frames")
frame_index = 0
if isinstance(frames, list) and frames:
first_frame = frames[0]
if isinstance(first_frame, int):
frame_index = first_frame
elif isinstance(first_frame, dict) and isinstance(first_frame.get("index"), int):
frame_index = int(first_frame["index"])
columns = max(1, image.width // frame_width)
frame_x = (frame_index % columns) * frame_width
frame_y = (frame_index // columns) * frame_height
frame_x = min(frame_x, max(0, image.width - frame_width))
frame_y = min(frame_y, max(0, image.height - frame_height))
return image.crop((frame_x, frame_y, frame_x + frame_width, frame_y + frame_height))
def load_texture_image(texture_path: Path) -> Image.Image:
image = Image.open(texture_path)
image = first_animation_frame(image, texture_path)
return image.convert("RGBA")
def resolve_texture_value(model: dict[str, Any], texture_ref: str, seen: set[str] | None = None) -> str | None:
if seen is None:
seen = set()
@@ -58,7 +121,7 @@ def load_image_or_placeholder(pack_root: Path, texture_ref: str | None, size: tu
texture_path = texture_ref_to_path(pack_root, texture_ref)
if texture_path.exists():
return Image.open(texture_path).convert("RGBA")
return load_texture_image(texture_path)
return placeholder_texture(texture_ref, size)
@@ -183,6 +246,10 @@ def apply_display_transform(point: tuple[float, float, float], display: dict[str
return x + center_x, y + center_y, z + center_z
def apply_preview_rotation(point: tuple[float, float, float]) -> tuple[float, float, float]:
return rotate_point(point, {"angle": PREVIEW_MODEL_ROTATION_Y, "axis": "y", "origin": [8.0, 8.0, 8.0]})
def element_corners(element: dict[str, Any]) -> list[tuple[float, float, float]]:
from_x, from_y, from_z = element["from"]
to_x, to_y, to_z = element["to"]
@@ -261,23 +328,22 @@ def resolve_face_texture(pack_root: Path, model: dict[str, Any], face: dict[str,
texture_path = texture_ref_to_path(pack_root, resolved)
texture_size = model.get("texture_size", [16, 16])
if texture_path.exists():
texture_image = Image.open(texture_path).convert("RGBA")
texture_image = load_texture_image(texture_path)
else:
texture_image = placeholder_texture(resolved, (int(texture_size[0]), int(texture_size[1])))
u1, v1, u2, v2 = face.get("uv", [0, 0, texture_image.width, texture_image.height])
source_width, source_height = texture_image.size
texture_size_x, texture_size_y = texture_size
scale_x = source_width / float(texture_size_x or source_width)
scale_y = source_height / float(texture_size_y or source_height)
scale_x = source_width / 16.0
scale_y = source_height / 16.0
left = float(min(u1, u2) * scale_x)
right = float(max(u1, u2) * scale_x)
top = float(min(v1, v2) * scale_y)
bottom = float(max(v1, v2) * scale_y)
crop_width = max(1, int(math.ceil(abs(right - left))))
crop_height = max(1, int(math.ceil(abs(bottom - top))))
crop_width = max(1, int(math.ceil(abs(right - left) * UV_SAMPLE_SCALE)))
crop_height = max(1, int(math.ceil(abs(bottom - top) * UV_SAMPLE_SCALE)))
crop = texture_image.transform(
(crop_width, crop_height),
Image.Transform.EXTENT,
@@ -296,19 +362,9 @@ def resolve_face_texture(pack_root: Path, model: dict[str, Any], face: dict[str,
return crop
def transform_face(texture: Image.Image, vertices: list[tuple[float, float, float]], canvas: Image.Image) -> tuple[Image.Image, tuple[int, int]] | None:
def transform_face(texture: Image.Image, vertices: list[tuple[float, float, float]], canvas: Image.Image) -> Image.Image | None:
projected = [point[:2] for point in vertices]
xs = [point[0] for point in projected]
ys = [point[1] for point in projected]
min_x = math.floor(min(xs))
min_y = math.floor(min(ys))
max_x = math.ceil(max(xs))
max_y = math.ceil(max(ys))
width = max(1, max_x - min_x)
height = max(1, max_y - min_y)
local = [(x - min_x, y - min_y) for x, y, _ in projected]
p0, p1, _, p3 = local
p0, p1, _, p3 = projected
source_width, source_height = texture.size
edge_a_x = p1[0] - p0[0]
edge_a_y = p1[1] - p0[1]
@@ -325,13 +381,18 @@ def transform_face(texture: Image.Image, vertices: list[tuple[float, float, floa
e = source_height * edge_a_x / det
f = source_height * (edge_a_y * p0[0] - edge_a_x * p0[1]) / det
warped = texture.transform((width, height), Image.Transform.AFFINE, (a, b, c, d, e, f), resample=Image.Resampling.BICUBIC)
mask = Image.new("L", (width, height), 0)
ImageDraw.Draw(mask).polygon(local, fill=255)
warped = texture.transform(
canvas.size,
Image.Transform.AFFINE,
(a, b, c, d, e, f),
resample=Image.Resampling.NEAREST,
)
mask = Image.new("L", canvas.size, 0)
ImageDraw.Draw(mask).polygon(projected, fill=255)
texture_alpha = warped.getchannel("A")
warped.putalpha(ImageChops.multiply(texture_alpha, mask))
return warped, (min_x, min_y)
return warped
def render_model(pack_root: Path, model_ref: str, output_path: Path, cache: dict[str, dict[str, Any]]) -> None:
@@ -353,6 +414,7 @@ def render_model(pack_root: Path, model_ref: str, output_path: Path, cache: dict
for element in model.get("elements", []):
corners = element_corners(element)
corners = [apply_display_transform(point, display) for point in corners]
corners = [apply_preview_rotation(point) for point in corners]
for corner in corners:
all_projected.append(corner)
transformed_elements.append((corners, element, model))
@@ -367,7 +429,8 @@ def render_model(pack_root: Path, model_ref: str, output_path: Path, cache: dict
canvas.save(output_path)
return
scale = min((canvas_size - 56) / width, (canvas_size - 56) / height)
preview_margin = canvas_size * PREVIEW_MARGIN_RATIO
scale = min((canvas_size - preview_margin) / width, (canvas_size - preview_margin) / height) * PREVIEW_SCALE_MULTIPLIER
bbox_xs: list[float] = []
bbox_ys: list[float] = []
for corner in all_projected:
@@ -396,51 +459,21 @@ def render_model(pack_root: Path, model_ref: str, output_path: Path, cache: dict
canvas.alpha_composite(shadow)
faces: list[tuple[float, list[tuple[float, float, float]], Image.Image]] = []
camera_direction = normalize_vector((-1.0, -1.0, -1.0))
for corners, element, model_data in transformed_elements:
for face_name, face in element.get("faces", {}).items():
vertices = face_vertices(corners, face_name)
if not vertices:
continue
normal = face_normal(vertices)
if normal[0] * camera_direction[0] + normal[1] * camera_direction[1] + normal[2] * camera_direction[2] >= 0:
continue
projected = [project_point(point, scale, offset_x, offset_y) for point in vertices]
texture = resolve_face_texture(pack_root, model_data, face)
faces.append((sort_face_key(projected), projected, texture))
faces.sort(key=lambda item: item[0])
for _, projected, texture in faces:
xs = [point[0] for point in projected]
ys = [point[1] for point in projected]
min_x = math.floor(min(xs))
min_y = math.floor(min(ys))
max_x = math.ceil(max(xs))
max_y = math.ceil(max(ys))
width = max(1, max_x - min_x)
height = max(1, max_y - min_y)
local = [(x - min_x, y - min_y) for x, y, _ in projected]
p0, p1, _, p3 = local
edge_a_x = p1[0] - p0[0]
edge_a_y = p1[1] - p0[1]
edge_b_x = p3[0] - p0[0]
edge_b_y = p3[1] - p0[1]
det = edge_a_x * edge_b_y - edge_a_y * edge_b_x
if abs(det) < 1e-5:
warped = transform_face(texture, projected, canvas)
if warped is None:
continue
source_width, source_height = texture.size
a = source_width * edge_b_y / det
b = -source_width * edge_b_x / det
c = source_width * (edge_b_x * p0[1] - edge_b_y * p0[0]) / det
d = -source_height * edge_a_y / det
e = source_height * edge_a_x / det
f = source_height * (edge_a_y * p0[0] - edge_a_x * p0[1]) / det
warped = texture.transform((width, height), Image.Transform.AFFINE, (a, b, c, d, e, f), resample=Image.Resampling.NEAREST)
mask = Image.new("L", (width, height), 0)
ImageDraw.Draw(mask).polygon(local, fill=255)
warped.putalpha(ImageChops.multiply(warped.getchannel("A"), mask))
canvas.alpha_composite(warped, (min_x, min_y))
canvas.alpha_composite(warped)
canvas.save(output_path)