DramaticShapeVoxelMod archival mirror · read-only
tools/building_images.py

building_images.py

tools/building_images.py4.1 KB100644de4ed8bb84
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
"""Composite the reference PNGs that
assets/docs/buildings/B##-unnamed-building.md files link to (img/B##.png,
img/B##_x{scale}.png, img/B##_atlas.png) but the repo does not ship, since
they land in the gitignored .scratchpad/ -- run this to regenerate them
locally.

Parses each doc's raw tile-id grid (the `local rows = {...}` Lua array), its
named tileset, and its scale factor straight out of the markdown, then
composites the building out of the tileset atlas the same way
building_voxels.py's sprite() does: paste each tile's 8x8 block, 16 tiles
per row. Emits, per building:
  - B##.png        native resolution
  - B##_x{N}.png   nearest-neighbor upscale, at the scale the doc references
  - B##_atlas.png  a strip of the building's distinct tiles, first-appearance
                    order (matches the doc's legend lettering)

    python mods/DRAMATIC_SHAPE/tools/building_images.py [outdir]

outdir defaults to .scratchpad/img at the repo root.
"""
import os
import re
import sys
import glob
from PIL import Image

MOD_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ROOT = os.path.dirname(os.path.dirname(MOD_ROOT))
DOCS = os.path.join(MOD_ROOT, "assets", "docs", "buildings")
TILESETS = os.path.join(ROOT, "assets", "generated", "tilesets")
PER_ROW = 16


def parse_doc(path):
    text = open(path, encoding="utf-8").read()
    bid = re.search(r"^# (B\d+)", text, re.M).group(1)
    scale = int(re.search(r"img/B\d+_x(\d+)\.png", text).group(1))
    tileset = re.search(r"assets/generated/tilesets/(\w+)\.png", text).group(1)
    lua = re.search(r"local rows = \{(.*?)\n\}", text, re.S).group(1)
    rows = []
    for line in lua.strip().splitlines():
        code = line.split("--")[0]
        nums = re.findall(r"\d+", code)
        if nums:
            rows.append([int(n) for n in nums])
    return bid, scale, tileset, rows


def composite(rows, atlas):
    h, w = len(rows), len(rows[0])
    im = Image.new("RGB", (w * 8, h * 8))
    for r, row in enumerate(rows):
        for c, t in enumerate(row):
            ax, ay = (t % PER_ROW) * 8, (t // PER_ROW) * 8
            im.paste(atlas.crop((ax, ay, ax + 8, ay + 8)), (c * 8, r * 8))
    return im


def atlas_strip(rows, atlas, cell=32, pad=2, per_row=12):
    """Every distinct tile the building uses, in first-appearance (row-major)
    order -- the same order the doc's legend assigns A, B, C, ... to."""
    seen, seenset = [], set()
    for row in rows:
        for t in row:
            if t not in seenset:
                seenset.add(t)
                seen.append(t)
    n = len(seen)
    cols = min(n, per_row)
    lines = (n + per_row - 1) // per_row
    W = cols * cell + (cols + 1) * pad
    H = lines * cell + (lines + 1) * pad
    img = Image.new("RGB", (W, H), (255, 255, 255))
    for i, t in enumerate(seen):
        ax, ay = (t % PER_ROW) * 8, (t // PER_ROW) * 8
        tile = atlas.crop((ax, ay, ax + 8, ay + 8)).resize((cell, cell), Image.NEAREST)
        col, line = i % per_row, i // per_row
        img.paste(tile, (pad + col * (cell + pad), pad + line * (cell + pad)))
    return img


def main():
    out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, ".scratchpad", "img")
    os.makedirs(out, exist_ok=True)

    atlas_cache = {}
    docs = sorted(
        glob.glob(os.path.join(DOCS, "B*-unnamed-building.md")),
        key=lambda p: int(re.search(r"B(\d+)", os.path.basename(p)).group(1)),
    )
    for path in docs:
        bid, scale, tileset, rows = parse_doc(path)
        if tileset not in atlas_cache:
            atlas_cache[tileset] = Image.open(
                os.path.join(TILESETS, tileset + ".png")
            ).convert("RGB")
        atlas = atlas_cache[tileset]

        native = composite(rows, atlas)
        native.save(os.path.join(out, f"{bid}.png"))

        big = native.resize((native.width * scale, native.height * scale), Image.NEAREST)
        big.save(os.path.join(out, f"{bid}_x{scale}.png"))

        atl = atlas_strip(rows, atlas)
        atl.save(os.path.join(out, f"{bid}_atlas.png"))

        print(f"{bid}: {tileset} {native.size} -> x{scale} {big.size}, atlas {atl.size}")


if __name__ == "__main__":
    main()