import json,struct,io
from pathlib import Path
from PIL import Image
import numpy as np
p=Path('le-mans-prototype.glb');raw=p.read_bytes();n=struct.unpack_from('<I',raw,12)[0];g=json.loads(raw[20:20+n]);blob=raw[28+n:]
required={'Body','Wheel_FL','Wheel_FR','Wheel_RL','Wheel_RR','SteeringWheel','Seat_Driver','Door_L','Door_R','Exhaust','Light_Head_L','Light_Head_R','Light_Tail_L','Light_Tail_R'}
nodes={x['name']:x for x in g['nodes']};assert set(nodes)==required,(set(nodes)-required,required-set(nodes));assert len(g['materials'])<=8;assert len(raw)<=12*1024*1024
for m in g['materials']:
 if m['name']=='Paint':
  assert m['pbrMetallicRoughness']['baseColorFactor']==[.03,.03,.035,1]
  assert m['extensions']['KHR_materials_clearcoat']['clearcoatFactor']==1
 if m['name']=='Glass':assert m['alphaMode']=='BLEND'
tri=0
for m in g['meshes']:
 for pr in m['primitives']:tri+=g['accessors'][pr['indices']]['count']//3
print('Triangles',tri)
assert 60000<=tri<=110000,tri
for name in ['Wheel_FL','Wheel_FR','Wheel_RL','Wheel_RR']:
 t=nodes[name]['translation'];assert abs(abs(t[2])-1.525)<1e-5;assert abs(t[1]-.355)<1e-5
for im in g['images']:
 b=g['bufferViews'][im['bufferView']];a=b.get('byteOffset',0);i=Image.open(io.BytesIO(blob[a:a+b['byteLength']]))
 assert max(i.size)<=1024,(im.get('name'),i.size)
 if im.get('name','').lower().endswith('_orm'):assert max(i.size)<=512
 print(im.get('name'),i.size)
print('Materials',len(g['materials']),'Meshes',len(g['meshes']),'Nodes',len(nodes),'Bytes',len(raw))
print('PASS contract checks')

m=json.loads(Path('nodes.json').read_text());bb=m['bounding_box'];dims=np.array(bb['max'])-np.array(bb['min']);assert np.allclose(dims,[2,1.15,5],atol=.001),dims
assert abs(bb['min'][1])<.00001
assert len(m['paints'])==6
assert Image.open('final.png').size==(1920,1080)
assert len(Path('NOTES.md').read_text().splitlines())==6
print('Dimensions',dims,'ground',bb['min'][1]);print('PASS delivered manifest and image checks')
mins=[];maxs=[]
for node in g['nodes']:
 assert 'rotation' not in node and 'scale' not in node and 'matrix' not in node
 if 'mesh' not in node:continue
 offset=np.array(node.get('translation',[0,0,0]))
 for prim in g['meshes'][node['mesh']]['primitives']:
  acc=g['accessors'][prim['attributes']['POSITION']];mins.append(np.array(acc['min'])+offset);maxs.append(np.array(acc['max'])+offset)
lo=np.min(mins,axis=0);hi=np.max(maxs,axis=0)
assert np.allclose(hi-lo,[2,1.15,5],atol=.001)
assert np.allclose(lo,bb['min'],atol=.00001) and np.allclose(hi,bb['max'],atol=.00001)
print('PASS actual GLB bounds and applied transforms',hi-lo)
