"""Offline hand normalization; dice geometry and all animation remain MASM.""" from pathlib import Path import struct import json from collections import deque from PIL import Image from pack_city_wake import figures ROOT = Path(__file__).resolve().parents[2] def dice_bounds(frame): points = {(x,y) for y in range(frame.height) for x in range(frame.width) if (lambda p: p[3] == 255 and p[0] > 210 and p[1] > 200 and p[2] > 180)(frame.getpixel((x,y)))} cubes = [] while points: start = points.pop() queue = deque([start]) body = [start] while queue: x,y = queue.popleft() for at in ((x-1,y),(x+1,y),(x,y-1),(x,y+1)): if at in points: points.remove(at) queue.append(at) body.append(at) if len(body) >= 100: xs,ys = zip(*body) cubes.append((min(xs),min(ys),max(xs)+1,max(ys)+1)) assert len(cubes) == 2, cubes return sorted(cubes) def main(): # Reuse the connected-silhouette recovery without nominal grid clipping. import pack_city_wake pack_city_wake.SRC = ROOT / "assets/sprites/dice" bodies = figures("hands-v2.png") scale = min(240/max(b.width for b in bodies),300/max(b.height for b in bodies)) frames = [] for body in bodies: body = body.resize((round(body.width*scale),round(body.height*scale)),Image.Resampling.NEAREST) frame = Image.new("RGBA",(256,320)) frame.paste(body,(256-body.width,320-body.height)) frames.append(frame) data = b"".join(f.tobytes("raw","BGRA") for f in frames) (ROOT / "assets/animations/dice/hands-v2.bin").write_bytes(struct.pack("<4s4I",b"ANM1",256,320,16,len(data))+data) frames[0].save(ROOT / "build/dice/hand-ready.png") bounds = [dice_bounds(f) for f in frames[:4]] # Release geometry starts at the centres of the last held pose, in the # same logical coordinate system as the MASM cube renderer. anchors = [((320+(b[0]+b[2])//2)//2,(90+(b[1]+b[3])//2)//2) for b in bounds[-1]] source = "; Generated by pack_dice_assets.py from the last held pose.\n" for i,(x,y) in enumerate(anchors,1): source += f"DICE_RELEASE_X{i} EQU {x}\nDICE_RELEASE_Y{i} EQU {y}\n" (ROOT / "src_graphics/dice_anchors.inc").write_text(source,encoding="ascii") (ROOT / "build/dice/held-bounds.json").write_text(json.dumps(bounds,indent=2)) sheet = Image.new("RGBA",(1024,1280)) for i,frame in enumerate(frames): sheet.paste(frame,((i%4)*256,(i//4)*320)) sheet.save(ROOT / "build/dice/hands-normalized.png") print("Packed sixteen full-cuff poses, two palm-contact dice and measured release anchors",anchors) if __name__ == "__main__": main()