/** Validate portable model containers before allowing renderer-side resource loads. */ export function inspectModel(bytes: Uint8Array, name: string) { if (bytes.byteLength < 12 || bytes.byteLength > 25 * 1024 * 1024) throw Error("Размер модели: 12 байт — 25 МБ"); let gltf: any; if (name.toLowerCase().endsWith(".gltf")) gltf = JSON.parse(new TextDecoder().decode(bytes)); else { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); if ( view.getUint32(0, true) !== 0x46546c67 || view.getUint32(4, true) !== 2 || view.getUint32(8, true) !== bytes.byteLength ) throw Error("Некорректный GLB v2"); if (bytes.byteLength < 20 || view.getUint32(16, true) !== 0x4e4f534a) throw Error("GLB не содержит JSON"); const length = view.getUint32(12, true); if (length > bytes.byteLength - 20) throw Error("Повреждён JSON GLB"); gltf = JSON.parse( new TextDecoder() .decode(bytes.subarray(20, 20 + length)) .replace(/\0+$/, ""), ); } if (gltf.asset?.version !== "2.0") throw Error("Поддерживается glTF 2.0"); if ( [...(gltf.buffers || []), ...(gltf.images || [])].some( (r: any) => r.uri && !r.uri.startsWith("data:"), ) ) throw Error("Экспортируй GLB со встроенными текстурами и буферами"); const compressed = [ "KHR_draco_mesh_compression", "EXT_meshopt_compression", "KHR_texture_basisu", ]; if ( [...(gltf.extensionsUsed || []), ...(gltf.extensionsRequired || [])].some( (x) => compressed.includes(x), ) ) throw Error( "Для автономного экспорта 0.1 используй GLB без Draco, Meshopt и KTX2", ); return { clips: (gltf.animations || []).map( (a: any, i: number) => a.name || "Animation " + i, ), skeletons: (gltf.skins || []).length, }; }