- 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>
80 lines
3.3 KiB
Python
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()
|