import type { MeshGeometryBuffer, MeshGeometryBufferField, MeshGeometryDelta, MeshGeometryRangePatch, } from "./web-engine"; export const meshGeometryBufferFields: readonly MeshGeometryBufferField[] = [ "positions", "indices", "normals", "triangleCornerIndices", "uvs", "colors", "triangleMaterialIndices", "triangleFaceIndices", "edgeVertexIndices", "tangents", "splitNormals", "sculptMask", "faceSets", ]; const maxUnchangedGap = 64; function changedRanges(meshId: string, field: MeshGeometryBufferField, before: ArrayBuffer, after: ArrayBuffer): MeshGeometryRangePatch[] { const oldBytes = new Uint8Array(before); const newBytes = new Uint8Array(after); const patches: MeshGeometryRangePatch[] = []; let cursor = 0; while (cursor < newBytes.length) { while (cursor < newBytes.length && oldBytes[cursor] === newBytes[cursor]) cursor += 1; if (cursor === newBytes.length) break; const start = cursor; let lastChanged = cursor; let unchanged = 0; cursor += 1; while (cursor < newBytes.length) { if (oldBytes[cursor] === newBytes[cursor]) { unchanged += 1; if (unchanged >= maxUnchangedGap) break; } else { lastChanged = cursor; unchanged = 0; } cursor += 1; } patches.push({ meshId, field, byteOffset: start, data: after.slice(start, lastChanged + 1) }); } return patches; } function requiresReplacement(before: MeshGeometryBuffer, after: MeshGeometryBuffer): boolean { return meshGeometryBufferFields.some((field) => { const oldBuffer = before[field]; const newBuffer = after[field]; return Boolean(oldBuffer) !== Boolean(newBuffer) || (oldBuffer?.byteLength ?? 0) !== (newBuffer?.byteLength ?? 0); }); } export function diffMeshGeometryBuffers(before: readonly MeshGeometryBuffer[], after: readonly MeshGeometryBuffer[]): MeshGeometryDelta { const previous = new Map(before.map((buffer) => [buffer.meshId, buffer])); const current = new Map(after.map((buffer) => [buffer.meshId, buffer])); const patches: MeshGeometryRangePatch[] = []; const replaced: MeshGeometryBuffer[] = []; const removed: string[] = []; for (const buffer of after) { const oldBuffer = previous.get(buffer.meshId); if (!oldBuffer || requiresReplacement(oldBuffer, buffer)) { replaced.push(buffer); continue; } for (const field of meshGeometryBufferFields) { const oldField = oldBuffer[field]; const newField = buffer[field]; if (oldField && newField) patches.push(...changedRanges(buffer.meshId, field, oldField, newField)); } } for (const meshId of previous.keys()) if (!current.has(meshId)) removed.push(meshId); return { schemaVersion: 1, patches, replaced, removed }; } function bufferByteLength(buffer: MeshGeometryBuffer): number { return meshGeometryBufferFields.reduce((total, field) => total + (buffer[field]?.byteLength ?? 0), 0); } export function applyMeshGeometryDelta(before: readonly MeshGeometryBuffer[], delta: MeshGeometryDelta): MeshGeometryBuffer[] { if (delta.schemaVersion !== 1) throw new Error("Unsupported MeshGeometryDelta schema"); const result = new Map(before.map((buffer) => [buffer.meshId, buffer])); for (const meshId of delta.removed) result.delete(meshId); for (const buffer of delta.replaced) result.set(buffer.meshId, buffer); const changed = new Map(); for (const patch of delta.patches) { const current = changed.get(patch.meshId) ?? result.get(patch.meshId); if (!current) throw new Error(`MeshGeometryDelta mesh not found: ${patch.meshId}`); const source = current[patch.field]; if (!source || patch.byteOffset < 0 || patch.byteOffset + patch.data.byteLength > source.byteLength) { throw new Error(`MeshGeometryDelta range is invalid: ${patch.meshId}.${patch.field}`); } const next = source.slice(0); new Uint8Array(next).set(new Uint8Array(patch.data), patch.byteOffset); const updated = { ...current, [patch.field]: next }; updated.byteLength = bufferByteLength(updated); changed.set(patch.meshId, updated); result.set(patch.meshId, updated); } return [...result.values()].sort((left, right) => left.meshId.localeCompare(right.meshId)); } export function cloneMeshGeometryBuffers(buffers: readonly MeshGeometryBuffer[]): MeshGeometryBuffer[] { return buffers.map((buffer) => { const clone = { ...buffer }; for (const field of meshGeometryBufferFields) { const value = buffer[field]; if (value) clone[field] = value.slice(0); } return clone; }); }