350 lines
12 KiB
Python
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())
|