"""Independent GLB 2.0 structure/position/scale QA; does not modify source assets.""" import struct,json,math,hashlib from pathlib import Path from collections import Counter ROOT=Path(__file__).resolve().parents[1] EXPECTED={f'{c}_{f}_{kind}.glb' for c in ['E01','E02','E03','E04'] for f in ['F','M'] for kind in ['mask','worn']} result={'scope':'GLB 2.0 structure, complete finite POSITION coordinates, accessor bounds, triangle index range, embedded resources, baked transforms, exact file set and physically scaled controller envelopes; glTF Y-up','scale_basis':'Source units 1=100mm; export bakes scale 0.1 into metres. Controller outer envelope is 95mm height x46mm width x19.25mm depth from the two source box halves; post-bevel dimensions must match. Overall scene dimensions include its separate off-face cable/controller, so they are recorded, while gross scale is anchored to the known controller rather than arbitrarily broad scene bounds.','files':[],'errors':[]} actual={p.name for p in (ROOT/'exports').glob('*.glb')} if actual!=EXPECTED:result['errors'].append({'missing_files':sorted(EXPECTED-actual),'unexpected_files':sorted(actual-EXPECTED)}) for p in sorted((ROOT/'exports').glob('*.glb')): try: data=p.read_bytes();magic,version,total=struct.unpack_from('<4sII',data);assert magic==b'glTF' and version==2 and total==len(data),'bad GLB header' off=12;chunks=[] while off0 and aoff+(a['count']-1)*stride+12<=view['byteLength'],'position range' low=[math.inf]*3;high=[-math.inf]*3 for i in range(a['count']): pt=struct.unpack_from('<3f',binchunk,start+i*stride);assert all(math.isfinite(x) for x in pt),'nonfinite position' for k in range(3):low[k]=min(low[k],pt[k]);high[k]=max(high[k],pt[k]) assert all(abs(low[k]-a['min'][k])<1e-6 and abs(high[k]-a['max'][k])<1e-6 for k in range(3)),'accessor min/max mismatch' if 'indices' in prim: ia=j['accessors'][prim['indices']];iv=j['bufferViews'][ia['bufferView']];isz,fmt={5121:(1,'B'),5123:(2,'H'),5125:(4,'I')}[ia['componentType']] assert ia['type']=='SCALAR' and ia['count']%3==0,'index type/count';istride=iv.get('byteStride',isz);ioff=ia.get('byteOffset',0);istart=iv.get('byteOffset',0)+ioff assert ioff+(ia['count']-1)*istride+isz<=iv['byteLength'],'index range' for ii in range(ia['count']):assert struct.unpack_from('<'+fmt,binchunk,istart+ii*istride)[0]0)==(kind=='worn'),'wrong mannequin inclusion' dims=[round((b-a)*1000,2) for a,b in zip(overall_min,overall_max)] result['files'].append({'file':p.name,'bytes':len(data),'sha256':hashlib.sha256(data).hexdigest(),'meshes':len(j['meshes']),'primitives':primitive_count,'vertices':vertex_count,'roles':dict(roles),'dimensions_mm_xyz_gltf':dims,'controller_dimensions_mm_xyz_gltf':[round(v,3) for v in cdims],'self_contained':True,'finite_positions':True,'accessor_bounds_match':True,'triangle_indices_valid':True,'unit_scale_check':True}) print('GLB_OK',p.name,dims,flush=True) except Exception as exc:result['errors'].append({'file':p.name,'error':str(exc)});print('GLB_FAIL',p.name,str(exc),flush=True) result['summary']={'files':len(result['files']),'expected_files':16,'pass':len(result['files'])==16 and not result['errors']} (ROOT/'source/export_qa.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n') print(json.dumps(result['summary']),flush=True) if not result['summary']['pass']:raise SystemExit(1)