Files
workinf_Blender_Wasm/web/protocol/mesh-cache.ts
2026-08-12 04:47:48 -04:00

139 lines
6.1 KiB
TypeScript

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<string, unknown> {
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<BufferField, ArrayBuffer>();
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,
};
});
}