121 lines
4.5 KiB
TypeScript
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;
|
|
});
|
|
}
|