#!/usr/bin/env python3 """ Kallast (GJ144d) heightmap + globe generator — Settled Reach Reads planet parameters from the wiki (wiki/star-systems/GJ-144/index.md), runs the FBM+Voronoi simulation pipeline, and produces: 1. Annotated equirectangular heightmap PNG (geographic features only) 2. Globe render PNG (ray-traced sphere with terrain data wrapped onto it) Both outputs share the same simulation run — terrain is computed once. Pipeline: body_definition_parser → parse wiki → body_def dict planet_simulation → FBM+Voronoi elevation, temperature, moisture, hillshade, rivers, biome classification render_heightmap → annotated equirectangular PNG planet_renderer → ray-traced globe PNG (terrain-driven surface) Usage: python3 generate_kallast.py [options] --small Fast iteration: 1024×512 heightmap, 512×512 globe --render-mode cartographic (NG map style) | photographic (orbital) Default outputs: /mnt/user-data/outputs/GJ144d_heightmap.png (4096×2048) /mnt/user-data/outputs/GJ144d_globe.png (2048×2048) """ import argparse import os import sys import time # ── Prototype pipeline on path ──────────────────────────────────────────────── _HERE = os.path.dirname(os.path.abspath(__file__)) _PROTO = os.path.join(_HERE, "prototype") if _PROTO not in sys.path: sys.path.insert(0, _PROTO) from body_definition_parser import parse_system from planet_simulation import simulate from render_heightmap import render_heightmap from planet_renderer import render_globe # ── Default paths ───────────────────────────────────────────────────────────── # Sprint worktree root is two levels above the spike dir. _SPRINT_ROOT = os.path.normpath(os.path.join(_HERE, "..", "..")) DEFAULT_WIKI = os.path.join(_SPRINT_ROOT, "wiki", "star-systems", "GJ-144", "index.md") DEFAULT_OUT_HM = "/mnt/user-data/outputs/GJ144d_heightmap.png" DEFAULT_OUT_GL = "/mnt/user-data/outputs/GJ144d_globe.png" DEFAULT_BODY = "GJ144d" def main(): parser = argparse.ArgumentParser( description="Generate Kallast (GJ144d) heightmap + globe", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) parser.add_argument( "--output", default=DEFAULT_OUT_HM, metavar="PATH", help="Heightmap PNG output path") parser.add_argument( "--globe-output", dest="globe_output", default=DEFAULT_OUT_GL, metavar="PATH", help="Globe PNG output path") parser.add_argument( "--body-id", default=DEFAULT_BODY, metavar="ID", help="Body ID to render (default: GJ144d)") parser.add_argument( "--wiki", default=DEFAULT_WIKI, metavar="PATH", help="Path to GJ-144 wiki index.md") parser.add_argument( "--out-w", type=int, default=4096, metavar="N", help="Heightmap output width (default: 4096)") parser.add_argument( "--out-h", type=int, default=2048, metavar="N", help="Heightmap output height (default: 2048)") parser.add_argument( "--globe-size", type=int, default=2048, metavar="N", help="Globe output size in px (default: 2048)") parser.add_argument( "--render-mode", choices=["cartographic", "photographic"], default="cartographic", help="Heightmap colour mode (default: cartographic)") parser.add_argument( "--small", action="store_true", help="Fast iteration: 1024×512 heightmap, 512×512 globe") args = parser.parse_args() if args.small: args.out_w = 1024 args.out_h = 512 args.globe_size = 512 t_start = time.time() # ── 1. Parse body definition from wiki ─────────────────────────────────── print(f"Parsing wiki: {args.wiki}") if not os.path.exists(args.wiki): raise SystemExit( f"Wiki not found: {args.wiki}\n" "Pass --wiki if running from a non-standard location.") bodies = parse_system(args.wiki) body_def = next((b for b in bodies if b["id"] == args.body_id), None) if body_def is None: available = [b["id"] for b in bodies] raise SystemExit( f"Body {args.body_id!r} not found in wiki.\n" f"Available: {available}\n" "Use --body-id to specify a different body.") pc = body_def.get("planet_class", "?") seed = body_def.get("seed", 0) print(f" {body_def['id']} class={pc} seed={seed}") # ── 2. Simulate terrain ────────────────────────────────────────────────── print("Simulating terrain…") t0 = time.time() terrain = simulate(body_def) sim_t = time.time() - t0 if not terrain: raise SystemExit(f"{args.body_id} is a gas giant — no terrain to render.") sea = terrain["sea_level"] nriv = len(terrain["rivers"]) print(f" Done in {sim_t:.1f}s " f"sea_level={sea:.3f} rivers={nriv}") if terrain.get("temperature_clamped"): raw_K = terrain.get("temperature_raw_K", "?") band = terrain.get("temperature_band_K", []) print(f" T_raw={raw_K}K clamped to {band} for {pc}") # ── 3. Render heightmap ────────────────────────────────────────────────── size_str = f"{args.out_w}×{args.out_h}" print(f"Rendering heightmap {size_str} ({args.render_mode})…") t1 = time.time() hm = render_heightmap( body_def, terrain, out_w=args.out_w, out_h=args.out_h, render_mode=args.render_mode) hm_t = time.time() - t1 os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True) hm.save(args.output, format="PNG") print(f" Saved: {args.output} ({hm_t:.1f}s)") # ── 4. Render globe ────────────────────────────────────────────────────── print(f"Rendering globe {args.globe_size}×{args.globe_size}…") t2 = time.time() glob = render_globe(body_def, terrain=terrain, size=args.globe_size) gl_t = time.time() - t2 os.makedirs(os.path.dirname(os.path.abspath(args.globe_output)), exist_ok=True) glob.save(args.globe_output, format="PNG") print(f" Saved: {args.globe_output} ({gl_t:.1f}s)") total = time.time() - t_start print(f"\nTotal: {total:.1f}s " f"(sim={sim_t:.1f}s hm={hm_t:.1f}s glob={gl_t:.1f}s)") if __name__ == "__main__": main()