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

@@ -1,4 +1,4 @@
import { applyGreasePencilEditorEdit, parseGreasePencilEditor } from "../../../protocol/grease-pencil-editor";
import { applyGreasePencilEditorEdit, applyGreasePencilPointTranslation, parseGreasePencilEditor } from "../../../protocol/grease-pencil-editor";
self.onmessage = () => {
const result: Record<string, unknown> = {};
@@ -11,5 +11,16 @@ self.onmessage = () => {
try { applyGreasePencilEditorEdit(base, { type: "SET_LAYER", revision: 7, layerId: "layer:Other" }); } catch (error) { result.stale = error instanceof Error ? error.message : String(error); }
try { parseGreasePencilEditor({ ...base, selectedPoints: [{ strokeIndex: 1, pointIndex: 1 }, { strokeIndex: 1, pointIndex: 1 }] }); } catch (error) { result.duplicate = error instanceof Error ? error.message : String(error); }
try { parseGreasePencilEditor({ ...base, selectedStrokeIndices: [-1] }); } catch (error) { result.negative = error instanceof Error ? error.message : String(error); }
try {
const editor = { ...base, selectedPoints: [{ strokeIndex: 0, pointIndex: 1 }] };
const translated = applyGreasePencilPointTranslation(editor, [{ cyclic: false, materialIndex: 0, points: [
{ position: [0, 0, 0], radius: 0.1, opacity: 1 },
{ position: [1, 2, 3], radius: 0.2, opacity: 0.8, vertexColor: [1, 0, 0, 1] },
] }], { type: "TRANSLATE_POINTS", revision: 0, translation: [0.5, -1, 2] });
result.translation = [translated.editor.revision, translated.strokes[0].points[0].position, translated.strokes[0].points[1]];
} catch (error) { result.translation = error instanceof Error ? error.message : String(error); }
try {
applyGreasePencilPointTranslation({ ...base, selectedPoints: [{ strokeIndex: 9, pointIndex: 1 }] }, [{ points: [{ position: [0, 0, 0] }] }], { type: "TRANSLATE_POINTS", revision: 0, translation: [1, 0, 0] });
} catch (error) { result.missingPoint = error instanceof Error ? error.message : String(error); }
self.postMessage(result);
};

View File

@@ -1,4 +1,4 @@
import { PAINT_BUDGET, parsePaintStroke, parseWeightPatch } from "../../../protocol/paint";
import { PAINT_BUDGET, applyUdimTilePatch, computePaintBrushWeights, parsePaintStroke, parseUdimTilePatch, parseWeightPatch } from "../../../protocol/paint";
const base = {
schemaVersion: 1,
@@ -11,12 +11,29 @@ const base = {
color: [1, 0.25, 0, 1],
};
self.onmessage = () => {
const result: Record<string, string> = {};
self.onmessage = async () => {
const result: Record<string, unknown> = {};
try { result.valid = parsePaintStroke(base).mode; } catch (error) { result.valid = error instanceof Error ? error.message : String(error); }
try { parsePaintStroke({ ...base, samples: [{ position: [0, 0, 0], barycentric: [0.1, 0.1, 0.1] }] }); } catch (error) { result.hit = error instanceof Error ? error.message : String(error); }
try { parsePaintStroke({ ...base, mode: "WEIGHT", vertexGroup: "Group", samples: new Array(PAINT_BUDGET.maxSamples + 1).fill(base.samples[0]) }); } catch (error) { result.budget = error instanceof Error ? error.message : String(error); }
try { result.weight = parseWeightPatch({ schemaVersion: 1, objectId: "object:Paint", revision: 3, vertexGroup: "Group", indices: [0, 1], values: [0.25, 0.75], normalize: true }).vertexGroup; } catch (error) { result.weight = error instanceof Error ? error.message : String(error); }
try {
result.brush = computePaintBrushWeights([
{ index: 2, position: [0, 0, 0], normal: [0, 0, 1] },
{ index: 1, position: [1, 0, 0], normal: [0, 0, 1] },
{ index: 3, position: [0.5, 0, 0], normal: [0, 0, 1], occluded: true },
{ index: 4, position: [3, 0, 0], normal: [0, 0, 1] },
], [0, 0, 0], 2, 0.8, { frontFaceOnly: true, viewDirection: [0, 0, -1] });
} catch (error) { result.brush = error instanceof Error ? error.message : String(error); }
try {
const baseTile = new Uint8Array(16);
const hash = async (bytes: Uint8Array) => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer)), (value) => value.toString(16).padStart(2, "0")).join("");
const expected = baseTile.slice(); expected.set([10, 20, 30, 255], 4);
const patch = { schemaVersion: 1, textureAssetId: "image:Paint", tile: 1001, revision: 2, width: 2, height: 2, format: "RGBA8", colorSpace: "SRGB", baseSha256: await hash(baseTile), resultSha256: await hash(expected), byteOffset: 4, bytes: new Uint8Array([10, 20, 30, 255]) };
parseUdimTilePatch(patch);
result.udim = Array.from(await applyUdimTilePatch(baseTile, patch, 2));
try { await applyUdimTilePatch(baseTile, patch, 3); } catch (error) { result.udimRevision = error instanceof Error ? error.message : String(error); }
try { await applyUdimTilePatch(new Uint8Array(16).fill(1), patch, 2); } catch (error) { result.udimStale = error instanceof Error ? error.message : String(error); }
} catch (error) { result.udim = error instanceof Error ? error.message : String(error); }
self.postMessage(result);
};

View File

@@ -1,4 +1,5 @@
import {
decodeBrowserTransformCacheFrame,
PHYSICS_FAMILIES,
PHYSICS_SIMULATION_BUDGET,
gatePhysicsExecution,
@@ -61,5 +62,17 @@ self.onmessage = () => {
result.solver = gatePhysicsExecution("FLUID", "LOCAL_SOLVER").issues[0]?.code;
result.manifest = gatePhysicsExecution("RIGID_BODY", "CACHE_MANIFEST").status;
result.familyCount = PHYSICS_FAMILIES.length;
try {
const bytes = new ArrayBuffer(16 + 72);
const view = new DataView(bytes);
view.setUint32(0, 0x31465442, true); view.setUint16(4, 1, true); view.setUint16(6, 16, true); view.setInt32(8, 7, true); view.setUint32(12, 1, true);
const id = new TextEncoder().encode("object:Cloth"); view.setUint8(16, id.length); new Uint8Array(bytes, 17, id.length).set(id);
[1, 2, 3, 0, 0, 0, 1, 1, 1, 1].forEach((value, index) => view.setFloat32(48 + index * 4, value, true));
const decoded = decodeBrowserTransformCacheFrame(bytes);
result.browserPlayback = [decoded.frame, decoded.objects[0].objectId, decoded.objects[0].translation];
view.setFloat32(48 + 3 * 4, 2, true);
try { decodeBrowserTransformCacheFrame(bytes); } catch (error) { result.browserRotation = error instanceof Error ? error.message : String(error); }
}
catch (error) { result.browserPlayback = error instanceof Error ? error.message : String(error); }
self.postMessage(result);
};

View File

@@ -13,6 +13,10 @@ const scope = self as unknown as {
const projectTransactions = new Map<string, Promise<void>>();
let opfsUsable: boolean | undefined;
interface WorkerLockManager {
request<T>(name: string, callback: () => Promise<T>): Promise<T>;
}
async function useOpfsForProject(projectId: string): Promise<boolean> {
if (opfsUsable !== undefined) return opfsUsable;
const workerNavigator = (self as unknown as { navigator?: Navigator }).navigator;
@@ -33,7 +37,10 @@ async function useOpfsForProject(projectId: string): Promise<boolean> {
async function withProjectTransaction<T>(projectId: string, operation: () => Promise<T>): Promise<T> {
projectLayout(projectId);
const previous = projectTransactions.get(projectId) ?? Promise.resolve();
const result = previous.catch(() => undefined).then(operation);
const result = previous.catch(() => undefined).then(() => {
const locks = (self as unknown as { navigator?: { locks?: WorkerLockManager } }).navigator?.locks;
return locks ? locks.request(`blender-web-project:${projectId}`, operation) : operation();
});
const tail = result.then(() => undefined, () => undefined);
projectTransactions.set(projectId, tail);
try {
@@ -197,6 +204,33 @@ async function saveProject(projectId: string, revision: number, buffer: ArrayBuf
const useOpfs = await useOpfsForProject(projectId);
const layout = useOpfs ? await ensureProjectLayout(projectId) : projectLayout(projectId);
const sha256 = await sha256Hex(buffer);
const existing = faultAt ? undefined : await readProjectRow(projectId);
if (existing && existing.revision > revision) {
throw new Error(`PROJECT_REVISION_CONFLICT: committed revision ${existing.revision} is newer than ${revision}`);
}
if (existing?.revision === revision) {
if (existing.bytes !== buffer.byteLength || existing.sha256 !== sha256) {
throw new Error(`PROJECT_REVISION_CONFLICT: revision ${revision} already has different content`);
}
if (existing.backend === "opfs" && useOpfs) {
const recovered = await recoverProjectBlend(projectId);
if (!recovered.manifest || recovered.manifest.revision !== revision || recovered.manifest.bytes !== existing.bytes || recovered.manifest.sha256 !== sha256) {
throw new Error(`PROJECT_REVISION_CONFLICT: revision ${revision} metadata does not match the OPFS commit`);
}
}
else if (existing.backend !== "indexeddb" || useOpfs || !existing.buffer || await sha256Hex(existing.buffer) !== sha256) {
throw new Error(`PROJECT_REVISION_CONFLICT: revision ${revision} backend does not match the committed project`);
}
return {
projectId,
bytes: existing.bytes,
revision,
persisted: true,
backend: existing.backend,
scenePath: existing.scenePath,
sha256,
};
}
if (useOpfs) {
const committed = await writeProjectBlend(projectId, revision, buffer, undefined, faultAt);
if (committed.manifest.sha256 !== sha256) throw new Error("Project commit digest mismatch");

View File

@@ -329,6 +329,7 @@ function assertFutureCapability(payload: Extract<WebEngineRequest["command"], {
}
case "setGreasePencilStrokes": {
if (typeof payload.dataId !== "string" || typeof payload.layerId !== "string" || !Number.isSafeInteger(payload.frame) || payload.frame < -1_000_000 || payload.frame > 1_000_000 || !Array.isArray(payload.strokes) || payload.strokes.length > 1_000_000) throw report("GREASE_PENCIL_SCHEMA_INVALID", "Grease Pencil stroke transaction is invalid");
if (payload.baseRevision !== undefined && payload.baseRevision !== currentSnapshot?.revision) throw report("REVISION_CONFLICT", "Grease Pencil stroke base revision does not match the current SceneIR");
let points = 0;
for (const [strokeIndex, stroke] of payload.strokes.entries()) {
if (!stroke || typeof stroke !== "object" || !Array.isArray(stroke.points) || stroke.points.length === 0 || stroke.points.length > 1_000_000 || (stroke.cyclic !== undefined && typeof stroke.cyclic !== "boolean") || (stroke.materialIndex !== undefined && (!Number.isSafeInteger(stroke.materialIndex) || stroke.materialIndex < 0))) throw report("GREASE_PENCIL_SCHEMA_INVALID", `Grease Pencil stroke ${strokeIndex} is invalid`);