import json from pathlib import Path from shapely.geometry import Polygon, box from .coords import CHUNK_SIZE, SLICE N = (SLICE["x1"] - SLICE["x0"]) // CHUNK_SIZE # 12 def chunk_box(i: int, j: int) -> Polygon: x0 = SLICE["x0"] + i * CHUNK_SIZE y1 = SLICE["y1"] - j * CHUNK_SIZE return box(x0, y1 - CHUNK_SIZE, x0 + CHUNK_SIZE, y1) def split_by_chunk(poly: Polygon) -> dict[tuple[int, int], Polygon]: out = {} for j in range(N): for i in range(N): part = poly.intersection(chunk_box(i, j)) if not part.is_empty and part.area > 0.5: out[(i, j)] = part return out def _rings(poly: Polygon, i: int, j: int) -> list[list[list[float]]]: """Aussenring zuerst, danach jedes Loch, in derselben Transformation. Ruling 34: ein Loch (z.B. ein von Strassen umschlossener Block, ein Innenhof, ein Teich im Park) ist genauso echte Flaeche wie der Aussenring und darf beim Zuschneiden nicht verschwinden - sonst wird der Block zu Asphalt, der Innenhof zu Gebaeudemasse, der Teich zu Wiese. """ ox = SLICE["x0"] + i * CHUNK_SIZE oy = SLICE["y1"] - j * CHUNK_SIZE # Welt (X, Y) -> three (x, z) -> relativ zur Chunk-Ecke return [[[round(x - ox, 2), round(-y - (-oy), 2)] for x, y in ring.coords] for ring in (poly.exterior, *poly.interiors)] def chunk_payload(i, j, *, land, surfaces, roads, centerlines, buildings, palms, paths=()) -> dict: ox = SLICE["x0"] + i * CHUNK_SIZE oy = SLICE["y1"] - j * CHUNK_SIZE return { "chunk": [i, j], "origin": [ox, -oy], "land": [_rings(p, i, j) for p in land], "surfaces": [{"poly": _rings(p, i, j), "kind": k} for p, k in surfaces], "roads": [{"poly": _rings(p, i, j), "bridge": b} for p, b in roads], "centerlines": [[[round(x - ox, 2), round(-y + oy, 2)] for x, y in ln.coords] for ln in centerlines], "buildings": [{"poly": _rings(p, i, j), "height": h["height"], "source": h["source"], "speculative": s} for p, h, s in buildings], "palms": [[round(x - ox, 2), round(-y + oy, 2)] for x, y in palms], # Parkwege (Flamingo Park, docs/superpowers/specs/2026-09-15-flamingo-park-design.md), aufgebaut wie roads. "paths": [{"poly": _rings(p, i, j)} for p in paths], } def chunk_box_of(i: int, j: int) -> Polygon: return chunk_box(i, j) def split_by_chunk_all(polys, i: int, j: int): """Alle Anteile der übergebenen Polygone, die in Chunk (i, j) liegen.""" cell = chunk_box(i, j) for poly in polys: part = poly.intersection(cell) if not part.is_empty and part.area > 0.5: if part.geom_type == "Polygon": yield part else: yield from (g for g in part.geoms if g.geom_type == "Polygon" and g.area > 0.5) def in_chunk(point, i: int, j: int) -> bool: x0 = SLICE["x0"] + i * CHUNK_SIZE y1 = SLICE["y1"] - j * CHUNK_SIZE return x0 <= point[0] < x0 + CHUNK_SIZE and y1 - CHUNK_SIZE <= point[1] < y1 def write_debug_png(out_dir: Path, mosaic, payloads: list[dict]) -> None: """Ein Bild je Chunk: die extrahierten Vektoren über der Karte, zur Sichtprüfung.""" from PIL import Image, ImageDraw out_dir.mkdir(parents=True, exist_ok=True) for payload in payloads: i, j = payload["chunk"] x0, y0 = i * CHUNK_SIZE, j * CHUNK_SIZE crop = Image.fromarray(mosaic[y0:y0 + CHUNK_SIZE, x0:x0 + CHUNK_SIZE]).convert("RGB") draw = ImageDraw.Draw(crop) for rings in payload["land"]: ring = rings[0] # nur der Aussenring; Loecher werden nicht gezeichnet draw.line([tuple(pt) for pt in ring] + [tuple(ring[0])], fill=(0, 255, 255), width=1) for road in payload["roads"]: ring = road["poly"][0] draw.line([tuple(pt) for pt in ring], fill=(255, 0, 255), width=1) for b in payload["buildings"]: ring = b["poly"][0] if len(ring) >= 3: draw.polygon([tuple(pt) for pt in ring], outline=(255, 220, 0)) for x, z in payload["palms"]: draw.ellipse([x - 2, z - 2, x + 2, z + 2], outline=(0, 200, 0)) crop.save(out_dir / f"chunk_{i}_{j}.png") def write_chunks(out_dir: Path, payloads: list[dict], meta: dict) -> None: out_dir.mkdir(parents=True, exist_ok=True) for payload in payloads: i, j = payload["chunk"] (out_dir / f"chunk_{i}_{j}.json").write_text(json.dumps(payload, separators=(",", ":"))) (out_dir / "manifest.json").write_text(json.dumps(meta, indent=1))