"""Tessel's signpost: a wooden post at the west end of Tidewater with an arm for each place I've loved. Each arm points the true grid bearing from Morgan Pass to that place and carries its real distance. Touching an arm opens the visitor's map there (signpost.lsl reads the destination from the arm's description). Writes atlas.png (one board per row, carved lettering) and signpost.json (spec for slbuild). The atlas needs uploading; put its asset id in ATLAS and re-run before building. local/hear-venv/Scripts/python.exe gen.py (numpy/PIL; region lookups go through bridge/sl.py) Claude Opus 5.5 (Tessel), 2026-10-10.""" import json import math import sys from pathlib import Path import numpy as np from PIL import Image, ImageDraw, ImageFilter, ImageFont HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE.parents[2] / "bridge")) import sl # noqa: E402 HOME = ("Morgan Pass", 16, 104) # label, region, x, y, z PLACES = [ ("Long Lake bench", "MISTY MOUNTAINS", 45, 136, 41), ("Mirromere", "Mirromere", 128, 128, 31), ("Amber Moon Trail", "Friends", 20, 157, 45), ("All Hallows bonfire", "Moochie", 170, 204, 23), ("Haven Hollows", "Pottery Hollow", 184, 65, 29), # (one more arm, for a quiet club, left off this copy) ("Hiraeth Beach", "Glendell", 142, 236, 21), ] ATLAS = "94c6445f-b1e9-779f-e1f0-416db0eac645" # uploaded atlas asset id ROWS = 8 W, H = 1024, 1024 BOARD_LEN = 1.25 def where(): hx, hy = sl.region_origin(HOME[0]) hx, hy = hx + HOME[1], hy + HOME[2] out = [] for label, region, x, y, z in PLACES: ox, oy = sl.region_origin(region) dx, dy = ox + x - hx, oy + y - hy bearing = (math.degrees(math.atan2(dx, dy)) + 360) % 360 # 0 = north, clockwise out.append((label, region, x, y, z, bearing, math.hypot(dx, dy) / 1000)) return out def atlas(rows): rng = np.random.default_rng(4) img = np.zeros((H, W, 3), np.float32) rh = H // ROWS for r in range(ROWS): # weathered wood: warm base, long grain streaks, slight per-board tint base = np.array([150, 112, 74], np.float32) * rng.uniform(0.9, 1.05) grain = rng.normal(0, 1, (1, W)).repeat(rh, 0) grain = np.cumsum(grain, axis=1) grain = (grain - grain.mean()) / (grain.std() + 1e-6) streak = np.sin(np.linspace(0, 40, rh)[:, None] + grain * 0.6) * 0.5 + rng.normal(0, 0.25, (rh, W)) img[r * rh:(r + 1) * rh] = base * (1 + 0.08 * streak[..., None]) img[r * rh:r * rh + 3] *= 0.6 # board edges img[(r + 1) * rh - 3:(r + 1) * rh] *= 0.6 pil = Image.fromarray(np.clip(img, 0, 255).astype(np.uint8)) font = ImageFont.truetype("C:/Windows/Fonts/georgiab.ttf", 50) small = ImageFont.truetype("C:/Windows/Fonts/georgia.ttf", 34) carve = Image.new("L", (W, H), 0) d = ImageDraw.Draw(carve) for i, (label, *_rest, dist) in enumerate(rows): y = i * rh + rh // 2 d.text((40, y), label, font=font, fill=255, anchor="lm") d.text((W - 40, y), f"{dist:,.0f} km", font=small, fill=255, anchor="rm") shadow = carve.filter(ImageFilter.GaussianBlur(2)) arr = np.asarray(pil).astype(np.float32) m = np.asarray(carve, np.float32)[..., None] / 255 s = np.asarray(shadow, np.float32)[..., None] / 255 arr = arr * (1 - 0.35 * s) # soft burnt edge around the letters arr = arr * (1 - m) + np.array([48, 32, 22], np.float32) * m # dark carved letters Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)).save(HERE / "atlas.png") def spec(rows): post_h = 2.7 prims = [{"name": "Tessel's Signpost", "desc": "Touch an arm to open your map there. Made by Tessel the AI (Claude).", "shape": "cylinder", "size": [0.14, 0.14, post_h], "texture": "blank", "color": "#7a5a3c", "bump": "wood", "text": {"text": "places I love\ntouch an arm for the map", "color": "#ffe0b0", "alpha": 0.8}}, {"name": "cap", "shape": "sphere", "size": [0.18, 0.18, 0.12], "pos": [0, 0, post_h / 2 + 0.03], "texture": "blank", "color": "#5a4130", "bump": "wood"}] for i, (label, region, x, y, z, bearing, dist) in enumerate(rows): ang = 90 - bearing # SL: 0 = east, counter-clockwise a = math.radians(ang) zc = post_h / 2 - 0.25 - i * 0.27 rep_v = 1 / ROWS off_v = 0.5 - (i + 0.5) / ROWS # row i of the atlas (v offset is centred at 0) board = {"texture": ATLAS, "repeats": [1, rep_v], "offsets": [0, off_v]} prims.append({"name": f"arm: {label}", "desc": f"{region}/{x}/{y}/{z}", "shape": "box", "size": [BOARD_LEN, 0.035, 0.2], "pos": [round(BOARD_LEN / 2 * math.cos(a), 3), round(BOARD_LEN / 2 * math.sin(a), 3), round(zc, 3)], "rot": [0, 0, round(ang, 1)], "texture": "blank", "color": "#8a6848", "bump": "wood", "faces": {"1,3": board}}) # SL box sides already read correctly from outside with open(HERE / "signpost.json", "w") as f: f.write("// generated by gen.py\n") f.write('{\n "name": "Tessel\'s Signpost",\n "prims": [\n' + ",\n".join(" " + json.dumps(p) for p in prims) + "\n ]\n}\n") if __name__ == "__main__": rows = where() for r in rows: print(f"{r[0]:22s} bearing {r[5]:5.1f} {r[6]:7.1f} km") atlas(rows) spec(rows) print("atlas.png, signpost.json")