Advance M8-M11 parity workflows
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions

This commit is contained in:
mes123456
2026-08-17 04:37:07 -04:00
parent 7c16b279ae
commit 0fe8d2bb56
324 changed files with 31920 additions and 863 deletions

View File

@@ -0,0 +1,156 @@
import { expect, test } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
const nonMeshBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/nonmesh_scene.blend");
test("M8-18 saves Volume Main and its NanoVDB binding, then reopens both production viewports", async ({ page }) => {
test.setTimeout(240_000);
await page.goto("/");
const blend = fs.readFileSync(nonMeshBlend);
const result = await page.evaluate(async ({ blendBytes }) => {
const [engineModule, storageModule, volumeModule, opfsModule, viewportModule, offscreenModule] = await Promise.all([
import("/src/engine-client/WebEngineClient.ts"),
import("/src/storage/StorageClient.ts"),
import("/src/volume/nanovdb-viewport.ts"),
import("/src/volume/nanovdb-opfs.ts"),
import("/src/three-adapter/viewport.ts"),
import("/src/three-adapter/offscreen-viewport.ts"),
]);
const { WebEngineClient } = engineModule;
const { StorageClient } = storageModule;
const {
loadAndCommitNanoVDBViewportAsset,
reopenNanoVDBViewportAssetFromOPFS,
} = volumeModule;
const { listVDBProjectBindings, pruneNanoVDBOPFS } = opfsModule;
const { ViewportRenderer } = viewportModule;
const { OffscreenViewportRenderer } = offscreenModule;
const projectId = "m8-volume-roundtrip";
const mainSourcePath = "//volumes/generated-smoke.vdb";
const bindingSourcePath = "volumes/generated-smoke.vdb";
const data = blendBytes.buffer.slice(blendBytes.byteOffset, blendBytes.byteOffset + blendBytes.byteLength) as ArrayBuffer;
const digest = async (value: ArrayBuffer): Promise<string> => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", value)), (byte) => byte.toString(16).padStart(2, "0")).join("");
const waitFor = async (condition: () => boolean, timeoutMs = 120_000): Promise<void> => {
const deadline = performance.now() + timeoutMs;
while (!condition()) {
if (performance.now() > deadline) throw new Error("M8-18 viewport volume timeout");
await new Promise((resolve) => setTimeout(resolve, 25));
}
};
const renderViewport = async (
backend: "main" | "offscreen",
snapshot: any,
geometryBuffers: any[],
nonMeshGeometryBuffers: any[],
asset: any,
): Promise<any> => {
const canvas = document.createElement("canvas");
canvas.style.cssText = "position:fixed;left:0;top:0;width:320px;height:240px;z-index:10000";
document.body.append(canvas);
const renderer: any = backend === "main" ? new ViewportRenderer(canvas) : new OffscreenViewportRenderer(canvas);
renderer.setSnapshot(snapshot, geometryBuffers, nonMeshGeometryBuffers);
renderer.setVolumeAssets([asset]);
await waitFor(() => ["ready", "blocked"].includes(canvas.dataset.volumeStatus ?? ""));
if (canvas.dataset.volumeStatus !== "ready") throw new Error(`${backend} volume ${canvas.dataset.volumeErrorCode ?? "blocked"}`);
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const visible = backend === "offscreen"
? Number(canvas.dataset.rendererPixels ?? 0)
: (() => {
const gl = renderer.renderer.getContext();
const pixels = new Uint8Array(64 * 64 * 4);
gl.readPixels(Math.max(0, Math.floor((gl.drawingBufferWidth - 64) / 2)), Math.max(0, Math.floor((gl.drawingBufferHeight - 64) / 2)), 64, 64, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
return Array.from({ length: 64 * 64 }, (_, index) => pixels[index * 4 + 3] > 0 && pixels[index * 4] + pixels[index * 4 + 1] + pixels[index * 4 + 2] > 40).filter(Boolean).length;
})();
let volumeObjects = 0;
if (backend === "main") renderer.scene.traverse((object: any) => { if (object.userData.nanoVDBVolume) volumeObjects += 1; });
const status = { backend, volumeStatus: canvas.dataset.volumeStatus, volumeCount: Number(canvas.dataset.volumeCount), volumeObjects, visible };
renderer.dispose();
canvas.remove();
return status;
};
await pruneNanoVDBOPFS(projectId, 0);
const firstEngine = new WebEngineClient({ timeoutMs: 60_000 });
await firstEngine.init();
const opened = await firstEngine.openBlend(data.slice(0));
const volume = opened.snapshot.nonMeshData.find((candidate: any) => candidate.type === "VOLUME");
if (!volume) throw new Error("M8-18 nonmesh fixture has no Volume Main data");
const properties = { displayDensity: 1.75, interpolation: "NEAREST" as const, stepSize: 0.125, velocityGrid: "velocity", velocityScale: 1.5 };
const changed = await firstEngine.applyCommand({ type: "setVolumeProperties", dataId: volume.id, sourcePath: mainSourcePath, ...properties });
const saved = await firstEngine.saveBlend();
const savedHash = await digest(saved);
const firstVolume = changed.snapshot.nonMeshData.find((candidate: any) => candidate.id === volume.id);
if (!firstVolume || firstVolume.sourcePath !== mainSourcePath) throw new Error("M8-18 Volume Main source path did not commit");
const storage = new StorageClient();
const savedProject = await storage.saveProject(projectId, 7, saved.slice(0));
storage.terminate();
firstEngine.terminate();
const committedStorage = new StorageClient();
const persisted = await committedStorage.readProject(projectId);
committedStorage.terminate();
const committedAsset = await loadAndCommitNanoVDBViewportAsset(
volume.id,
bindingSourcePath,
"/volumes/generated-smoke.nanovdb.json",
"/volumes/generated-smoke.nvdb",
{ projectId, sourceBlendSha256: savedHash },
new AbortController().signal,
);
const bindings = await listVDBProjectBindings(projectId);
const binding = bindings.find((candidate: any) => candidate.bundleSha256 === committedAsset.manifest.bundleSha256);
if (!binding) throw new Error("M8-18 VDB binding was not discoverable after commit");
const reopenedEngine = new WebEngineClient({ timeoutMs: 60_000 });
await reopenedEngine.init();
const reopened = await reopenedEngine.openBlend(persisted.buffer.slice(0));
const reopenedVolume = reopened.snapshot.nonMeshData.find((candidate: any) => candidate.id === volume.id);
if (!reopenedVolume) throw new Error("M8-18 reopened Main has no Volume data");
const reopenedAsset = await reopenNanoVDBViewportAssetFromOPFS(
volume.id,
bindingSourcePath,
{ projectId, sourceBlendSha256: savedHash },
new AbortController().signal,
);
const assetHashes = async (asset: any): Promise<string[]> => Promise.all(asset.grids.map((grid: any) => digest(grid.data)));
const committedHashes = await assetHashes(committedAsset);
const reopenedHashes = await assetHashes(reopenedAsset);
const mainViewport = await renderViewport("main", reopened.snapshot, reopened.geometryBuffers, reopened.nonMeshGeometryBuffers ?? [], reopenedAsset);
const offscreenViewport = await renderViewport("offscreen", reopened.snapshot, reopened.geometryBuffers, reopened.nonMeshGeometryBuffers ?? [], reopenedAsset);
const reopenedBytesHash = await digest(persisted.buffer);
reopenedEngine.terminate();
await pruneNanoVDBOPFS(projectId, 0);
return {
project: { revision: savedProject.revision, bytes: savedProject.bytes, persisted: savedProject.persisted, backend: savedProject.backend, hash: savedProject.sha256 },
savedHash,
reopenedStorage: { revision: persisted.revision, bytes: persisted.bytes, sha256: persisted.sha256, backend: persisted.backend, recovered: persisted.recovered, bytesHash: reopenedBytesHash },
main: { id: firstVolume.id, sourcePath: firstVolume.sourcePath, properties: firstVolume.volumeProperties },
reopenedMain: { id: reopenedVolume.id, sourcePath: reopenedVolume.sourcePath, properties: reopenedVolume.volumeProperties, revision: reopened.snapshot.revision },
binding: { projectId: binding.projectId, sourcePath: binding.sourcePath, sourceBlendSha256: binding.sourceBlendSha256, bundleSha256: binding.bundleSha256, manifestSha256: binding.manifestSha256 },
asset: { dataId: reopenedAsset.dataId, bundleSha256: reopenedAsset.manifest.bundleSha256, committedHashes, reopenedHashes, gridCount: reopenedAsset.grids.length },
viewports: { main: mainViewport, offscreen: offscreenViewport },
};
}, { blendBytes: new Uint8Array(blend) });
console.log("nanovdb-volume-roundtrip", JSON.stringify({
project: result.project,
savedHash: result.savedHash,
reopenedStorage: result.reopenedStorage,
binding: result.binding,
asset: result.asset,
viewports: result.viewports,
}));
expect(result.project).toMatchObject({ revision: 7, persisted: true, backend: "opfs", hash: result.savedHash });
expect(result.project.bytes).toBeGreaterThan(0);
expect(result.reopenedStorage).toMatchObject({ revision: 7, sha256: result.savedHash, backend: "opfs", recovered: false, bytesHash: result.savedHash });
expect(result.main).toMatchObject({ id: result.reopenedMain.id, sourcePath: "//volumes/generated-smoke.vdb", properties: { displayDensity: 1.75, interpolation: "NEAREST", stepSize: 0.125, velocityGrid: "velocity", velocityScale: 1.5 } });
expect(result.reopenedMain).toMatchObject(result.main);
expect(result.binding).toMatchObject({ projectId: "m8-volume-roundtrip", sourcePath: "volumes/generated-smoke.vdb", sourceBlendSha256: result.savedHash });
expect(result.asset).toMatchObject({ dataId: result.main.id, committedHashes: result.asset.reopenedHashes });
expect(result.asset.gridCount).toBeGreaterThan(0);
expect(result.viewports.main).toMatchObject({ backend: "main", volumeStatus: "ready", volumeCount: 1, volumeObjects: 1 });
expect(result.viewports.main.visible).toBeGreaterThan(50);
expect(result.viewports.offscreen).toMatchObject({ backend: "offscreen", volumeStatus: "ready", volumeCount: 1 });
expect(result.viewports.offscreen.visible).toBeGreaterThan(10);
});