Advance bounded editor and cache workflows

This commit is contained in:
mes123456
2026-08-12 19:31:41 -04:00
parent d54d5dd913
commit 3da1dfc804
19 changed files with 563 additions and 50 deletions

View File

@@ -61,6 +61,19 @@ export interface PhysicsFamilyCapabilityIR {
serverJob: "BLOCKED";
}
export interface BrowserTransformCacheObjectIR {
objectId: string;
translation: [number, number, number];
rotationQuaternion: [number, number, number, number];
scale: [number, number, number];
}
export interface BrowserTransformCacheFrameIR {
schemaVersion: 1;
frame: number;
objects: BrowserTransformCacheObjectIR[];
}
export class PhysicsSimulationValidationError extends Error {
readonly code: ErrorCode;
@@ -237,3 +250,46 @@ export function selectPhysicsCacheFrame(system: PhysicsSystemIR, requestedFrame:
}
return { cacheKey: cache.cacheKey, frame: requestedFrame };
}
const BROWSER_TRANSFORM_CACHE_MAGIC = 0x31465442; // BTF1
const BROWSER_TRANSFORM_CACHE_HEADER_BYTES = 16;
const BROWSER_TRANSFORM_CACHE_OBJECT_BYTES = 72;
export function decodeBrowserTransformCacheFrame(value: ArrayBuffer): BrowserTransformCacheFrameIR {
if (!(value instanceof ArrayBuffer) || value.byteLength < BROWSER_TRANSFORM_CACHE_HEADER_BYTES) {
throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", "Browser transform cache frame is truncated");
}
const view = new DataView(value);
if (view.getUint32(0, true) !== BROWSER_TRANSFORM_CACHE_MAGIC || view.getUint16(4, true) !== 1 || view.getUint16(6, true) !== BROWSER_TRANSFORM_CACHE_HEADER_BYTES) {
throw new PhysicsSimulationValidationError("PROTOCOL_MISMATCH", "Unsupported browser transform cache frame schema");
}
const frameNumber = view.getInt32(8, true);
const count = view.getUint32(12, true);
if (frameNumber < -1_000_000 || frameNumber > 1_000_000 || count > PHYSICS_SIMULATION_BUDGET.maxSystems || value.byteLength !== BROWSER_TRANSFORM_CACHE_HEADER_BYTES + count * BROWSER_TRANSFORM_CACHE_OBJECT_BYTES) {
throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", "Browser transform cache frame length or count is invalid");
}
const decoder = new TextDecoder("utf-8", { fatal: true });
const objects: BrowserTransformCacheObjectIR[] = [];
const ids = new Set<string>();
for (let index = 0; index < count; index += 1) {
const offset = BROWSER_TRANSFORM_CACHE_HEADER_BYTES + index * BROWSER_TRANSFORM_CACHE_OBJECT_BYTES;
const idLength = view.getUint8(offset);
if (idLength === 0 || idLength > 31) throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Browser transform cache object ${index} has an invalid ID length`);
let objectId: string;
try { objectId = decoder.decode(new Uint8Array(value, offset + 1, idLength)); }
catch { throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Browser transform cache object ${index} has invalid UTF-8`); }
if (!objectId.startsWith("object:") || ids.has(objectId)) throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Browser transform cache object ${index} has an invalid or duplicate ID`);
ids.add(objectId);
const numbers = Array.from({ length: 10 }, (_, component) => view.getFloat32(offset + 32 + component * 4, true));
if (numbers.some((component) => !Number.isFinite(component))) throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Browser transform cache object ${index} has non-finite transforms`);
const quaternionLength = Math.hypot(numbers[3], numbers[4], numbers[5], numbers[6]);
if (Math.abs(quaternionLength - 1) > 1e-3 || numbers.slice(7).some((component) => component === 0)) throw new PhysicsSimulationValidationError("PHYSICS_CACHE_FRAME_MISMATCH", `Browser transform cache object ${index} has an invalid rotation or scale`);
objects.push({
objectId,
translation: numbers.slice(0, 3) as [number, number, number],
rotationQuaternion: numbers.slice(3, 7) as [number, number, number, number],
scale: numbers.slice(7, 10) as [number, number, number],
});
}
return { schemaVersion: 1, frame: frameNumber, objects };
}