cocos-farm/tools/render_canvas_preview.py

350 lines
12 KiB
Python

#!/usr/bin/env python3
"""Render a diagnostic Stage 1 canvas preview from source evidence."""
from __future__ import annotations
import json
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).resolve().parents[1]
SOURCE_JSON = ROOT / "source_export" / "stage1_world_camera_source.json"
LAND_SOURCE_JSON = ROOT / "source_export" / "stage1_visible_lands_source.json"
TOP_HUD_SOURCE_JSON = ROOT / "source_export" / "stage1_top_hud_source.json"
OUT_DIR = ROOT / "artifacts" / "canvas_preview"
PREVIEW_PATH = OUT_DIR / "stage1_world_camera_source_preview.png"
REPORT_PATH = OUT_DIR / "stage1_world_camera_source_preview_report.json"
def load_json(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def flatten_nodes(nodes: list[dict]) -> dict[str, dict]:
return {item["path"]: item for item in nodes}
def world_position(path: str, nodes: dict[str, dict]) -> tuple[float, float]:
parts = path.split("/")
current = []
x = 0.0
y = 0.0
for part in parts:
current.append(part)
node = nodes.get("/".join(current))
if node:
x += float(node["localPosition"]["x"])
y += float(node["localPosition"]["y"])
return x, y
def draw_source_sprite(
output: Image.Image,
source_path: str,
world_x: float,
world_y: float,
node_width: float,
node_height: float,
anchor_x: float,
anchor_y: float,
camera: dict,
) -> dict:
width, height = output.size
with Image.open(source_path) as source_image:
image = source_image.convert("RGBA").resize(
(int(round(node_width)), int(round(node_height))),
Image.Resampling.LANCZOS,
)
screen_x = width / 2 + (world_x - float(camera["sourcePosition"]["x"])) - anchor_x * node_width
screen_y = height / 2 - (world_y - float(camera["sourcePosition"]["y"])) - (1 - anchor_y) * node_height
output.alpha_composite(image, (int(round(screen_x)), int(round(screen_y))))
return {
"worldX": world_x,
"worldY": world_y,
"width": node_width,
"height": node_height,
"anchorX": anchor_x,
"anchorY": anchor_y,
"screenX": int(round(screen_x)),
"screenY": int(round(screen_y)),
}
def uint32_to_rgba(value: int) -> tuple[int, int, int, int]:
return (value & 255, (value >> 8) & 255, (value >> 16) & 255, (value >> 24) & 255)
def load_font(font_size: float) -> ImageFont.ImageFont:
for font_path in [
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/System/Library/Fonts/Supplemental/Arial.ttf",
]:
path = Path(font_path)
if path.exists():
return ImageFont.truetype(str(path), int(round(font_size)))
return ImageFont.load_default()
def top_hud_mock_label(top_hud_evidence: dict, source: dict) -> str:
mock_user = top_hud_evidence["mockUser"]
mock_field = source["label"].get("mockField")
if mock_field == "nickname":
return str(mock_user["nickname"])
if mock_field == "level":
return f" {mock_user['level']} "
if mock_field == "experience":
return f"{mock_user['exp']}/{mock_user['expMax']}"
if mock_field == "goldDisplay":
return str(mock_user["goldDisplay"])
if mock_field == "diamond":
return str(mock_user["diamond"])
return str(source["label"]["string"])
def ui_rect(output_size: tuple[int, int], world_x: float, world_y: float, node_width: float, node_height: float, anchor_x: float, anchor_y: float) -> tuple[float, float, float, float]:
width, height = output_size
screen_x = width / 2 + world_x - anchor_x * node_width
screen_y = height / 2 - world_y - (1 - anchor_y) * node_height
return screen_x, screen_y, screen_x + node_width, screen_y + node_height
def draw_ui_sprite(
output: Image.Image,
source_path: str,
world_x: float,
world_y: float,
node_width: float,
node_height: float,
anchor_x: float,
anchor_y: float,
) -> dict:
with Image.open(source_path) as source_image:
image = source_image.convert("RGBA").resize(
(int(round(node_width)), int(round(node_height))),
Image.Resampling.LANCZOS,
)
left, top, _, _ = ui_rect(output.size, world_x, world_y, node_width, node_height, anchor_x, anchor_y)
output.alpha_composite(image, (int(round(left)), int(round(top))))
return {
"worldX": world_x,
"worldY": world_y,
"width": node_width,
"height": node_height,
"anchorX": anchor_x,
"anchorY": anchor_y,
"screenX": int(round(left)),
"screenY": int(round(top)),
}
def draw_ui_label(output: Image.Image, source: dict, text: str) -> dict:
label = source["label"]
size = source["size"]
anchor = source.get("anchor", {"x": 0.5, "y": 0.5})
world = source.get("worldPosition")
if not world:
raise ValueError(f"missing worldPosition for label {source['path']}")
left, top, right, bottom = ui_rect(
output.size,
float(world["x"]),
float(world["y"]),
float(size["x"]),
float(size["y"]),
float(anchor["x"]),
float(anchor["y"]),
)
draw = ImageDraw.Draw(output)
font = load_font(float(label["fontSize"]))
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
horizontal_align = int(label.get("horizontalAlign", 1))
if horizontal_align == 0:
x = left
elif horizontal_align == 2:
x = right - text_width
else:
x = left + ((right - left) - text_width) / 2
y = top + ((bottom - top) - text_height) / 2
draw.text((int(round(x)), int(round(y))), text, font=font, fill=uint32_to_rgba(int(label["colorUint32"])))
return {
"worldX": float(world["x"]),
"worldY": float(world["y"]),
"width": float(size["x"]),
"height": float(size["y"]),
"screenX": int(round(left)),
"screenY": int(round(top)),
"text": text,
}
def main() -> int:
evidence = load_json(SOURCE_JSON)
land_evidence = load_json(LAND_SOURCE_JSON)
top_hud_evidence = load_json(TOP_HUD_SOURCE_JSON)
width = int(evidence["canvas"]["width"])
height = int(evidence["canvas"]["height"])
camera = evidence["camera"]
nodes = flatten_nodes(evidence["nodes"])
top_hud_nodes = flatten_nodes(top_hud_evidence["nodes"])
assets = {item["id"]: item for item in [*evidence["assets"], *land_evidence["assets"], *top_hud_evidence["assets"]]}
output = Image.new("RGBA", (width, height), (0, 0, 0, 0))
rendered = []
for path in [
"root/scene/farm_scene_v3/Scene/BgLayer/defaultbg",
"root/scene/farm_scene_v3/Scene/FgLayer/default",
]:
node = nodes[path]
asset = assets[node["spriteAssetId"]]
world_x, world_y = world_position(path, nodes)
node_width = float(node["size"]["x"])
node_height = float(node["size"]["y"])
result = draw_source_sprite(
output,
asset["sourcePath"],
world_x,
world_y,
node_width,
node_height,
float(node["pivot"]["x"]),
float(node["pivot"]["y"]),
camera,
)
rendered.append(
{
"path": path,
"assetId": node["spriteAssetId"],
**result,
}
)
for land in sorted(land_evidence["visibleLands"], key=lambda item: item["cell"]["sourceIndex"]):
icon = land["icon"]
asset_id = land["mockRender"]["assetId"]
icon_asset_id = "land_locked" if asset_id == "land_extend" else asset_id
asset = assets[icon_asset_id]
result = draw_source_sprite(
output,
asset["sourcePath"],
float(icon["worldPosition"]["x"]),
float(icon["worldPosition"]["y"]),
float(icon["size"]["x"]),
float(icon["size"]["y"]),
float(icon["anchor"]["x"]),
float(icon["anchor"]["y"]),
camera,
)
rendered.append(
{
"path": icon["path"],
"gridName": land["gridName"],
"landId": land["landId"],
"state": land["mockRender"]["state"],
"assetId": icon_asset_id,
"mockAssetId": asset_id,
**result,
}
)
if asset_id == "land_extend":
board_asset = assets["land_extend"]
board_width = float(board_asset["pixelSize"]["width"])
board_height = float(board_asset["pixelSize"]["height"])
board_result = draw_source_sprite(
output,
board_asset["sourcePath"],
float(icon["worldPosition"]["x"]),
float(icon["worldPosition"]["y"]),
board_width,
board_height,
float(board_asset["pivot"]["x"]),
float(board_asset["pivot"]["y"]),
camera,
)
rendered.append(
{
"path": f"{icon['path']}/expand_board",
"gridName": land["gridName"],
"landId": land["landId"],
"state": land["mockRender"]["state"],
"assetId": "land_extend",
**board_result,
}
)
for source in top_hud_evidence["nodes"]:
if not source.get("spriteAssetId") or not source.get("size"):
continue
asset = assets[source["spriteAssetId"]]
world = source.get("worldPosition")
if world:
world_x = float(world["x"])
world_y = float(world["y"])
else:
world_x, world_y = world_position(source["path"], top_hud_nodes)
anchor = source.get("anchor", {"x": 0.5, "y": 0.5})
result = draw_ui_sprite(
output,
asset["sourcePath"],
world_x,
world_y,
float(source["size"]["x"]),
float(source["size"]["y"]),
float(anchor["x"]),
float(anchor["y"]),
)
rendered.append(
{
"path": source["path"],
"assetId": source["spriteAssetId"],
"uiLayer": True,
**result,
}
)
for source in top_hud_evidence["nodes"]:
if not source.get("label"):
continue
result = draw_ui_label(output, source, top_hud_mock_label(top_hud_evidence, source))
rendered.append(
{
"path": source["path"],
"uiLayer": True,
"label": True,
**result,
}
)
OUT_DIR.mkdir(parents=True, exist_ok=True)
output.save(PREVIEW_PATH)
alpha = output.getchannel("A")
non_transparent = sum(1 for value in alpha.getdata() if value)
report = {
"status": "pass",
"previewPath": str(PREVIEW_PATH),
"width": width,
"height": height,
"camera": {
"sourcePosition": camera["sourcePosition"],
"orthographicHeight": camera["orthographicHeight"],
"defaultZoom": 1.0,
},
"renderedNodeCount": len(rendered),
"visibleLandCount": len(land_evidence["visibleLands"]),
"topHudNodeCount": len(top_hud_evidence["nodes"]),
"topHudMockUser": top_hud_evidence["mockUser"],
"excludedStaticGridCount": land_evidence["excludedStaticGrid"]["visualLandCellCount"],
"nonTransparentPixels": non_transparent,
"renderedNodes": rendered,
}
REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())