#!/usr/bin/env python3 """ plot_wallmap.py -- render the JSON dumped by tools/diag_wallmap.luau as a top-down PNG. WHY THIS EXISTS: the Studio viewport cannot be screenshotted on this machine (Vinegar/Wine on nested X display :1, DXVK-rendered -> XGetImage returns black). For "is there a gap / does it look jumbled / which way does it face" questions, plotting the datamodel geometry top-down is both possible and more precise than a screenshot. Usage: python3 tools/plot_wallmap.py /tmp/wallmap_raw.txt /tmp/wallmap.png ["Title"] The input file is whatever rbx.call('execute_luau', ...) returned for diag_wallmap.luau -- it may be JSON or a Python-repr dict, both are handled. """ import ast import json import math import subprocess import sys STYLE = { "Road": ("#3a3a44", 0.5), "Ramp": ("#4a4a58", 0.5), "Apron": ("#2e2e36", 0.5), "Barrier": ("#ffffff", 0.95), "EdgeRail": ("#8a8a9a", 0.95), "KillWall": ("#ff4444", 0.95), "PARingWall": ("#00e5ff", 0.95), "PARingRail": ("#0090a0", 0.95), "Median": ("#ffd400", 0.95), "MedianFence": ("#c0a000", 0.95), } # Draw decks first so walls land on top of them. ORDER = [ "Road", "Ramp", "Apron", "Median", "MedianFence", "KillWall", "EdgeRail", "Barrier", "PARingWall", "PARingRail", ] LEGEND = [ ("PARingWall (ring parapet)", "#00e5ff"), ("Barrier (mainline)", "#ffffff"), ("KillWall", "#ff4444"), ("Median", "#ffd400"), ("Road/Ramp deck", "#3a3a44"), ] def load(path): raw = open(path).read() start = raw.find("{") if start < 0: raise SystemExit(f"no payload found in {path}") body = raw[start:] try: return json.loads(body) except json.JSONDecodeError: # The bridge often hands back a Python repr (single quotes, True/None). return ast.literal_eval(body) def analyse(parts): """Print the numbers that answer 'is it sealed' before drawing anything.""" ring = [p for p in parts if p["k"] == "PARingWall"] if not ring: return None, None cx = sum(p["x"] for p in ring) / len(ring) cz = sum(p["z"] for p in ring) / len(ring) rad = [math.hypot(p["x"] - cx, p["z"] - cz) for p in ring] split = (min(rad) + max(rad)) / 2 inner = [p for p in ring if math.hypot(p["x"] - cx, p["z"] - cz) < split] outer = [p for p in ring if math.hypot(p["x"] - cx, p["z"] - cz) >= split] print(f"PA centre ~({cx:.0f},{cz:.0f}) radii {min(rad):.0f}..{max(rad):.0f}") print(f"inner-edge walls {len(inner)}/72 outer-edge walls {len(outer)}/72") angs = sorted(math.degrees(math.atan2(p["z"] - cz, p["x"] - cx)) % 360 for p in inner) gaps = [] for i, a in enumerate(angs): nxt = angs[(i + 1) % len(angs)] d = (nxt - a) % 360 if d > 7.5: # one segment is 5 deg; >7.5 means at least one is absent gaps.append((round(a, 1), round(nxt, 1), round(d, 1))) print("INNER-wall gaps >7.5deg (a usable exit shows here):", gaps or "NONE - sealed") road = sum(1 for p in parts if p["k"] == "Road") bar = sum(1 for p in parts if p["k"] == "Barrier") if road: # One barrier per side of every deck segment => healthy ratio ~2.0. print(f"mainline Road={road} Barrier={bar} ratio={bar/road:.2f} (healthy ~2.0)") return cx, cz def render(parts, cx, cz, out_png, title, span=1000.0, size=1400): def sx(x): return (x - cx) / span * (size / 2) + size / 2 def sy(z): return (z - cz) / span * (size / 2) + size / 2 svg = [ f'' f'' ] for kind in ORDER: col, op = STYLE.get(kind, ("#888888", 0.9)) for p in parts: if p["k"] != kind: continue w = max(p["sx"] / span * (size / 2), 1.5) h = max(p["sz"] / span * (size / 2), 1.5) # Negate yaw: world +Z runs down the screen in this projection. svg.append( f'' f'' ) svg.append(f'') svg.append( f'{title}' ) for i, (lab, col) in enumerate(LEGEND): yy = 64 + i * 25 svg.append( f'' f'{lab}' ) svg.append("") svg_path = out_png.rsplit(".", 1)[0] + ".svg" open(svg_path, "w").write("\n".join(svg)) # rsvg-convert gives cleaner output but is often not installed; fall back to # ImageMagick. FileNotFoundError (binary absent) must be caught too -- a # returncode check alone does not cover it. try: rc = subprocess.run( ["rsvg-convert", "-w", str(size), svg_path, "-o", out_png], capture_output=True, ).returncode except FileNotFoundError: rc = 1 if rc != 0: subprocess.run(["convert", "-density", "96", svg_path, out_png], check=True) print("wrote", out_png) def main(): src = sys.argv[1] if len(sys.argv) > 1 else "/tmp/wallmap_raw.txt" dst = sys.argv[2] if len(sys.argv) > 2 else "/tmp/wallmap.png" title = sys.argv[3] if len(sys.argv) > 3 else "Daikoku PA -- wall map" data = load(src) parts = data["parts"] print("counts (whole map):", data.get("counts")) print("plotted parts:", len(parts)) cx, cz = analyse(parts) if cx is None: raise SystemExit("no PARingWall parts found -- nothing to centre on") render(parts, cx, cz, dst, title) if __name__ == "__main__": main()