# Usage: python3 tools/floorre/explain_t1l4m002.py tools/floorre OUT.png (WAD via env WAD, default t1l4m002 path in floor.py; needs Pillow, numpy) # Explainer: Kuzco floating next to the crate in t1l4m002 (builds on floorre/plot.py) import sys, math sys.path.insert(0, sys.argv[1]) # floorre dir (floor.py, corners.py) OUT = sys.argv[2] sys.argv = sys.argv[:1] from floor import * # mp, trak, floor_test from corners import corners from PIL import Image, ImageDraw, ImageFont SS = 2 # supersampling F = lambda n, b=False: ImageFont.truetype('DejaVuSans-Bold.ttf' if b else 'DejaVuSans.ttf', n * SS) K = (57.204, 2.474, 43.733) T = 443 TX, TY, TZ, ROT, MI, _ = mp['A'][T] recs = [r for g in trak[MI].coll for r in g] def xf(B): rot = B[1]; tx, ty, tz = B[3] / 4096, B[4] / 4096, B[5] / 4096 a = -rot / 4096 * 2 * math.pi; s, c = math.sin(a), math.cos(a) return lambda p: (tx + c * p[0] - s * p[2], ty + p[1], tz + s * p[0] + c * p[2]) RED, BLUE, MAG, PINK = (215, 25, 25), (20, 90, 230), (210, 0, 210), (255, 70, 150) def panel(X0, Z0, span, size, detail): S = size / span img = Image.new('RGB', (size * SS, size * SS), 'white'); d = ImageDraw.Draw(img, 'RGBA') P = lambda x, z: ((x - X0) * S * SS, (size - (z - Z0) * S) * SS) # rendered terrain coloured by height (like the agent's plot) for t in range(mp['N']): B = mp['B'][t] if not (X0 - 2 <= B[3] / 4096 <= X0 + span + 2 and Z0 - 2 <= B[5] / 4096 <= Z0 + span + 2): continue m = trak[mp['midx'][t]]; f = xf(B) for fc in sorted(m.faces, key=lambda fc: sum(m.verts[v][1] for v in fc['v'])): Q = [f((m.verts[v][0], m.verts[v][1], -m.verts[v][2])) for v in fc['v']] h = sum(p[1] for p in Q) / len(Q); g = int(max(0, min(255, (h - 1.5) / 2 * 255))) d.polygon([P(p[0], p[2]) for p in Q], fill=(g, 200 - g // 2, 255 - g, 255), outline=(130, 130, 130) if not detail else None) # cell grid for k in range(int(X0) - 1, int(X0 + span) + 2): d.line([P(k, Z0 - 1), P(k, Z0 + span + 1)], fill=(0, 140, 0), width=SS * (2 if detail else 1)) for k in range(int(Z0) - 1, int(Z0 + span) + 2): d.line([P(X0 - 1, k), P(X0 + span + 1, k)], fill=(0, 140, 0), width=SS * (2 if detail else 1)) return img, d, P, S def collision(d, P, labels, sliver=None, fsz=12): f = xf(mp['B'][T]) if sliver: for (x, z, w, i) in sliver: a, b = P(x, z), P(x + w, z - w) d.rectangle([a[0], a[1], b[0], b[1]], fill=PINK + (170,)) for i, r in enumerate(recs): C = [f(p) for p in corners(r)] col = RED if i in (11, 15) else BLUE if i == 2 else (20, 20, 20) wid = 3 if i in (2, 11, 15) else 1 pts = [P(p[0], p[2]) for p in C] if i in (11, 15): d.polygon(pts, fill=RED + (70,)) d.line(pts + pts[:1], fill=col, width=wid * SS) if labels: cx = sum(p[0] for p in pts) / len(pts); cy = sum(p[1] for p in pts) / len(pts) lim = d.im.size if not (10 * SS < cx < lim[0] - 10 * SS and 10 * SS < cy < lim[1] - (80 if labels == 'zoom' else 10) * SS): continue txt = f'poly {i} · mat {r[1]}' if labels == 'zoom' and i in (2, 11, 15) else str(i) d.text((cx, cy), txt, fill=col, font=F(fsz, i in (2, 11, 15)), anchor='mm', stroke_width=SS * 2, stroke_fill='white') def sliver_cells(x0, z0, x1, z1, step): out = []; tx, ty, tz = int(round(TX * 4096)), int(round(TY * 4096)), int(round(-TZ * 4096)) z = z0 while z < z1: x = x0 while x < x1: if int(x) == 57 and int(z) == 43: _, res = floor_test(int(round(x * 4096)), int(round(2.27 * 4096)), int(round(z * 4096)), tx, ty, tz, ROT, recs) best = None for i, i7, ok, vals, tol, Y in res: if ok and i7 >= -0x80 and (best is None or i7 < best[0]): best = (i7, i) if best and best[1] in (11, 15): out.append((x, z, step, best[1])) x += step z += step return out W = 1500 # --- left: overview (3 x 3 cells) --- ov, d, P, S = panel(56.0, 42.0, 3.0, 640, False) collision(d, P, 'all', fsz=11) for t in range(mp['N']): B = mp['B'][t]; x, z = B[3] / 4096, B[5] / 4096 if 56 <= x <= 59 and 42 <= z <= 45: d.text(P(x - 0.45, z - 0.38), f'tile {t}', fill=(110, 0, 140), font=F(12, True), stroke_width=SS * 2, stroke_fill='white') k = P(K[0], K[2]); d.ellipse([k[0] - 7 * SS, k[1] - 7 * SS, k[0] + 7 * SS, k[1] + 7 * SS], fill=MAG, outline='black', width=SS) ZX0, ZZ0, ZSP = 57.06, 43.16, 0.76 a, b = P(ZX0, ZZ0 + ZSP), P(ZX0 + ZSP, ZZ0) d.rectangle([a, b], outline=(255, 140, 0), width=3 * SS) ov = ov.resize((640, 640), Image.LANCZOS) # --- right: zoom on the crate's side with both needles --- zm, d, P, S = panel(ZX0, ZZ0, ZSP, 820, True) collision(d, P, 'zoom', sliver=sliver_cells(ZX0, ZZ0, ZX0 + ZSP, ZZ0 + ZSP, 0.0012), fsz=15) k = P(K[0], K[2]); r = 9 * SS d.ellipse([k[0] - r, k[1] - r, k[0] + r, k[1] + r], fill=MAG, outline='black', width=2 * SS) d.text((k[0], k[1] + 30 * SS), 'Kuzco\n57.204 / 43.733\n(measured live)', fill=MAG, font=F(15, True), anchor='ma', align='center', stroke_width=SS * 2, stroke_fill='white') tip = P(57.315, 43.729) y = tip[1] - 34 * SS d.line([(k[0], y), (tip[0], y)], fill='black', width=2 * SS) for xq in (k[0], tip[0]): d.line([(xq, y - 8 * SS), (xq, y + 8 * SS)], fill='black', width=2 * SS) d.text(((k[0] + tip[0]) / 2, y - 6 * SS), '0.11 outside poly 15', fill='black', font=F(14, True), anchor='md', stroke_width=SS * 2, stroke_fill='white') d.text((P(57.38, 43.80)), 'needle tips 13.5°', fill=RED, font=F(14, True), stroke_width=SS * 2, stroke_fill='white') d.text((P(ZX0 + 0.01, ZZ0 + 0.035)), 'pink = allowance: where the engine still takes the needle as floor\n(edge tolerance ≈ 0.02 → ≈ 0.19 long, 0.01 wide at a 13.5° tip)', fill=(190, 20, 110), font=F(14, True), stroke_width=SS * 2, stroke_fill='white') zm = zm.resize((820, 820), Image.LANCZOS) out = Image.new('RGB', (20 + 640 + 20 + 820 + 20, 820 + 40), 'white') d = ImageDraw.Draw(out) out.paste(ov, (20, 20 + 90)); out.paste(zm, (680, 20)) d.text((20, 20 + 60), 'Overview (3 × 3 cells), orange box = zoom →', fill='black', font=ImageFont.truetype('DejaVuSans-Bold.ttf', 17)) out.save(OUT); print('saved', OUT, out.size)