PROCITY/pipeline/skin_sheet.py
m3ultra 7fe6a4a40e Lane E R39 (3/n): the notes + the audit — the magpie recommendation with its arithmetic, the arcade's measured kit, and the sizing law promoted to a standing instruction
- LANE_E_NOTES §39: RECOMMENDATION TAKE THE GLB, on three findings in the order they decide it
  (80.5% of drawn frames are PERCHED and the perch is the pose E's asset already is; E reads pied
  at 64/48/32 px and under a 25% smear while B reads as a dark aeroplane; and B's white is
  geometrically buried — the wing bar sits 3 mm ABOVE a horizontal wing plane, so from underneath,
  which is where the player always is, the black wing occludes its own white flash).
- Three asks filed for Lane B, none of them edits: magpie.js's two stale 154-tri comments (:9, :123
  against a measured 182); the wing bar's side (y 0.048 -> ~0.042 is +0 tris and fixes ?classic and
  ?noassets whatever Fable rules); and two GROUND_FALLBACK entries so the arcade is not grey
  without assets.
- The arcade measured, with the awning-post finding: two post rows 1.00 m apart down a 5.00 m lane.
- AUDIT §Round 39 §0 makes the sizing law a STANDING INSTRUCTION with two new clauses: draws are
  counted per CLASS (town-wide InstancedMesh +1 vs per-chunk +1 x 25 live chunks), and for a SKIN
  the budget is the SLOT, not bytes (36 facade-atlas slots, 22 used, 14 free — past 36 skins.js
  silently REUSES one).
- pipeline/skin_sheet.py: wrap-preview + measured seam error, the check R38's stretched grounds
  needed before the harvest instead of after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:53:55 +10:00

80 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""PROCITY Lane E — skin_sheet.py (R39)
Contact sheet for 2-D skins that answers the two questions a thumbnail cannot: **does it tile**, and
**what aspect is it actually displayed at**. R38 shipped four grounds and only afterwards recorded
that the eight flux-era ones are 512x279 and stretch under a square UV tile — that is the class of
defect this sheet exists to catch before a harvest, not after.
PY=~/Documents/MODELBEAST/venvs/mflux/bin/python
$PY pipeline/skin_sheet.py OUT.png "title" FILE.jpg "caption" [FILE "caption" ...] [--note "line" ...]
Each row: the skin as shipped, a 3x3 wrap preview (what RepeatWrapping does to it), and the measured
wrap-seam error - mean |difference| between the opposite edges, 0-255, printed as a number rather
than left to the eye.
"""
import os
import sys
import textwrap
import numpy as np
from PIL import Image, ImageDraw
PAD, LBL, TILE = 12, 15, 220
def seam(a):
x = float(np.abs(a[:, 0].astype(np.float32) - a[:, -1].astype(np.float32)).mean())
y = float(np.abs(a[0].astype(np.float32) - a[-1].astype(np.float32)).mean())
return x, y
def main():
out_path, title = sys.argv[1], sys.argv[2]
args = sys.argv[3:]
notes = []
if "--note" in args:
i = args.index("--note")
notes, args = args[i + 1:], args[:i]
rows = [(args[i], args[i + 1]) for i in range(0, len(args), 2)]
W = PAD + TILE + PAD + TILE + PAD + 700
H = 26 + len(rows) * (TILE + LBL + PAD) + PAD + LBL * (sum(len(n) // 110 + 2 for n in notes) + 1)
sheet = Image.new("RGB", (W, H), (24, 24, 26))
d = ImageDraw.Draw(sheet)
d.text((PAD, 6), title, fill=(240, 240, 240))
y = 26
for path, cap in rows:
im = Image.open(path).convert("RGB")
a = np.asarray(im)
sx, sy = seam(a)
# as shipped, letterboxed into a square cell so a non-square skin LOOKS non-square
cell = Image.new("RGB", (TILE, TILE), (40, 40, 44))
s = im.copy()
s.thumbnail((TILE, TILE), Image.LANCZOS)
cell.paste(s, ((TILE - s.width) // 2, (TILE - s.height) // 2))
sheet.paste(cell, (PAD, y))
# 3x3 wrap preview
t = Image.new("RGB", (im.width * 3, im.height * 3))
for i in range(3):
for j in range(3):
t.paste(im, (i * im.width, j * im.height))
sheet.paste(t.resize((TILE, TILE), Image.LANCZOS), (PAD + TILE + PAD, y))
x0 = PAD + 2 * (TILE + PAD)
d.text((x0, y + 2), os.path.basename(path), fill=(250, 210, 120))
d.text((x0, y + 2 + LBL), f"{im.width}x{im.height} {im.width / im.height:.2f}:1 "
f"{os.path.getsize(path)} B wrap-seam x={sx:.1f} y={sy:.1f} (0-255)",
fill=(180, 180, 180))
for k, line in enumerate(textwrap.wrap(cap, 96)):
d.text((x0, y + 2 + LBL * (2 + k)), line, fill=(215, 215, 215))
d.text((PAD + 3, y + TILE + 1), "as shipped", fill=(150, 150, 150))
d.text((PAD + TILE + PAD + 3, y + TILE + 1), "3x3 wrap (RepeatWrapping)", fill=(150, 150, 150))
y += TILE + LBL + PAD
for line in notes:
for part in textwrap.wrap(line, 150):
d.text((PAD, y), part, fill=(190, 190, 190))
y += LBL
sheet.crop((0, 0, W, min(sheet.height, y + PAD))).save(out_path)
print(f"skin sheet -> {out_path}")
main()