#!/usr/bin/env python3 """ Stitch keystone.guru tile pyramid (z=4 by default) into one WebP per dungeon-floor. Reads `data/kg/_summary.json` (produced by kg_fetch.py) for the grid bounds. Output: web/assets/maps/[_floor].webp """ from __future__ import annotations import argparse import concurrent.futures import json import sys from pathlib import Path from PIL import Image ROOT = Path(__file__).resolve().parent.parent KG_DIR = ROOT / "data" / "kg" WEB_MAPS = ROOT / "web" / "assets" / "maps" TILE_W, TILE_H = 384, 256 def stitch_one(args): tile_key, floor_info, zoom, all_floors_count, quality = args floor = floor_info["index"] cols, rows = floor_info["cols"], floor_info["rows"] src_dir = KG_DIR / tile_key / f"floor{floor}" / f"z{zoom}" canvas = Image.new("RGBA", (cols * TILE_W, rows * TILE_H), (0, 0, 0, 255)) missing = 0 for x in range(cols): for y in range(rows): tile_path = src_dir / f"{x}_{y}.png" if not tile_path.exists(): missing += 1 continue with Image.open(tile_path) as t: canvas.paste(t.convert("RGBA"), (x * TILE_W, y * TILE_H)) suffix = f"_floor{floor}" if all_floors_count > 1 else "" out = WEB_MAPS / f"{tile_key}{suffix}.webp" out.parent.mkdir(parents=True, exist_ok=True) canvas.convert("RGB").save(out, "WEBP", quality=quality, method=4) return tile_key, floor, canvas.size, missing def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--quality", type=int, default=85) ap.add_argument("--workers", type=int, default=4) args = ap.parse_args() summary_path = KG_DIR / "_summary.json" if not summary_path.exists(): print(f"missing {summary_path} — run kg_fetch.py first", file=sys.stderr) return 1 summary = json.loads(summary_path.read_text()) jobs = [] for d in summary["dungeons"]: if not d.get("floors"): continue for floor_info in d["floors"]: jobs.append((d["tile_key"], floor_info, d["max_zoom"], len(d["floors"]), args.quality)) print(f"stitching {len(jobs)} dungeon-floors with {args.workers} workers...") with concurrent.futures.ProcessPoolExecutor(max_workers=args.workers) as pool: for tile_key, floor, size, missing in pool.map(stitch_one, jobs): note = f" ({missing} missing tiles)" if missing else "" print(f" {tile_key} f{floor}: {size[0]}×{size[1]}{note}") print(f"done → {WEB_MAPS}") return 0 if __name__ == "__main__": raise SystemExit(main())