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

121 lines
4.5 KiB
TypeScript

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<string, MeshGeometryBuffer>();
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;
});
}