import type { MeshGeometryBuffer, WebEngineLODLevelResult } from "./web-engine"; const bufferFields = ["positions", "indices", "normals", "triangleCornerIndices", "uvs", "colors", "triangleMaterialIndices"] as const; type BufferField = typeof bufferFields[number]; interface BufferDescriptor { field: BufferField; offset: number; byteLength: number; } interface GeometryDescriptor { meshId: string; byteLength: number; buffers: BufferDescriptor[]; } interface LevelDescriptor { level: number; meshId: string; triangleBudget: number; outputTriangleCount: number; outputVertexCount: number; geometry: GeometryDescriptor[]; } interface CacheHeader { schemaVersion: 1; levels: LevelDescriptor[]; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } export function encodeLODGeometry(levels: readonly WebEngineLODLevelResult[]): ArrayBuffer { if (levels.length === 0) throw new Error("LOD geometry cache requires at least one level"); const chunks: ArrayBuffer[] = []; let offset = 0; const header: CacheHeader = { schemaVersion: 1, levels: levels.map((level, expectedLevel) => { if (level.level !== expectedLevel || level.geometryBuffers.length === 0) throw new Error("LOD cache levels must be contiguous and contain geometry"); return { level: level.level, meshId: level.meshId, triangleBudget: level.triangleBudget, outputTriangleCount: level.outputTriangleCount, outputVertexCount: level.outputVertexCount, geometry: level.geometryBuffers.map((geometry) => { const descriptors: BufferDescriptor[] = []; for (const field of bufferFields) { const value = geometry[field]; if (!(value instanceof ArrayBuffer)) continue; descriptors.push({ field, offset, byteLength: value.byteLength }); chunks.push(value); offset += value.byteLength; } if (!descriptors.some((descriptor) => descriptor.field === "positions") || !descriptors.some((descriptor) => descriptor.field === "indices")) { throw new Error("LOD cache geometry requires positions and indices"); } return { meshId: geometry.meshId, byteLength: geometry.byteLength, buffers: descriptors }; }), }; }), }; const headerBytes = new TextEncoder().encode(JSON.stringify(header)); const output = new ArrayBuffer(4 + headerBytes.byteLength + offset); const view = new DataView(output); view.setUint32(0, headerBytes.byteLength, true); const bytes = new Uint8Array(output); bytes.set(headerBytes, 4); let writeOffset = 4 + headerBytes.byteLength; for (const chunk of chunks) { bytes.set(new Uint8Array(chunk), writeOffset); writeOffset += chunk.byteLength; } return output; } export function decodeLODGeometry(data: ArrayBuffer): WebEngineLODLevelResult[] { if (data.byteLength < 5) throw new Error("LOD geometry cache is truncated"); const headerLength = new DataView(data).getUint32(0, true); if (headerLength === 0 || headerLength > data.byteLength - 4 || headerLength > 16 * 1024 * 1024) throw new Error("LOD geometry cache header is invalid"); const parsed = JSON.parse(new TextDecoder().decode(new Uint8Array(data, 4, headerLength))) as unknown; if (!isRecord(parsed) || parsed.schemaVersion !== 1 || !Array.isArray(parsed.levels) || parsed.levels.length === 0) { throw new Error("LOD geometry cache schema is invalid"); } const payloadOffset = 4 + headerLength; return parsed.levels.map((rawLevel, expectedLevel) => { if (!isRecord(rawLevel) || rawLevel.level !== expectedLevel || typeof rawLevel.meshId !== "string" || !Array.isArray(rawLevel.geometry)) { throw new Error("LOD geometry cache level is invalid"); } const geometryBuffers = rawLevel.geometry.map((rawGeometry): MeshGeometryBuffer => { if (!isRecord(rawGeometry) || typeof rawGeometry.meshId !== "string" || !Array.isArray(rawGeometry.buffers)) { throw new Error("LOD geometry cache entry is invalid"); } const decoded = new Map(); for (const rawBuffer of rawGeometry.buffers) { if (!isRecord(rawBuffer) || !bufferFields.includes(rawBuffer.field as BufferField) || !Number.isSafeInteger(rawBuffer.offset) || !Number.isSafeInteger(rawBuffer.byteLength)) { throw new Error("LOD geometry cache buffer descriptor is invalid"); } const offset = rawBuffer.offset as number; const byteLength = rawBuffer.byteLength as number; if (offset < 0 || byteLength < 0 || payloadOffset + offset + byteLength > data.byteLength || decoded.has(rawBuffer.field as BufferField)) { throw new Error("LOD geometry cache buffer range is invalid"); } decoded.set(rawBuffer.field as BufferField, data.slice(payloadOffset + offset, payloadOffset + offset + byteLength)); } const positions = decoded.get("positions"); const indices = decoded.get("indices"); if (!positions || !indices) throw new Error("LOD geometry cache is missing topology buffers"); return { schemaVersion: 1, meshId: rawGeometry.meshId, byteLength: typeof rawGeometry.byteLength === "number" ? rawGeometry.byteLength : [...decoded.values()].reduce((sum, value) => sum + value.byteLength, 0), positions, indices, normals: decoded.get("normals"), triangleCornerIndices: decoded.get("triangleCornerIndices"), uvs: decoded.get("uvs"), colors: decoded.get("colors"), triangleMaterialIndices: decoded.get("triangleMaterialIndices"), }; }); if (typeof rawLevel.triangleBudget !== "number" || typeof rawLevel.outputTriangleCount !== "number" || typeof rawLevel.outputVertexCount !== "number") { throw new Error("LOD geometry cache statistics are invalid"); } return { level: expectedLevel, meshId: rawLevel.meshId, triangleBudget: rawLevel.triangleBudget, outputTriangleCount: rawLevel.outputTriangleCount, outputVertexCount: rawLevel.outputVertexCount, geometryBuffers, }; }); }