Advance WBV2 domain parity through P12
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-28 06:38:27 +08:00
parent f18f80d0b9
commit 2abe7cc220
64 changed files with 19894 additions and 17 deletions

View File

@@ -0,0 +1,85 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const evidenceRoot = path.join(root, "tests/golden/WBV2-P12-003");
const materialFixture = fs.readFileSync(path.join(root, "tests/files/web/generated/WBV2-P3-001-MATERIAL.PBR_UV.blend"));
const animationFixture = fs.readFileSync(path.join(root, "tests/files/web/generated/WBV2-P3-002-ANIMATION.TRS_ACTION.blend"));
const digest = (bytes: Uint8Array): string => crypto.createHash("sha256").update(bytes).digest("hex");
test("WBV2-P12-003 executes material and animation mutations through compiled WASM Main", async ({ page }) => {
test.setTimeout(240_000);
await page.goto("/");
const result = await page.evaluate(async (input) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const buffer = (values: number[]): ArrayBuffer => Uint8Array.from(values).buffer;
const digestBytes = async (value: ArrayBuffer): Promise<string> => { const hash = await crypto.subtle.digest("SHA-256", value); return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join(""); };
const shape = (error: any) => ({ code: error?.code, message: error?.message });
const materialClient = new WebEngineClient({ timeoutMs: 60_000 });
const materialReopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
const animationClient = new WebEngineClient({ timeoutMs: 60_000 });
const animationReopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
try {
const materialOpened = await materialClient.openBlend(buffer(input.materialBytes));
const materialId = "material:WBV2P3001PrincipledMaterial";
const meshId = "mesh:WBV2P3001MaterialCubeMesh";
const sourceMaterial: any = materialOpened.snapshot.materials.find((candidate: any) => candidate.id === materialId);
const sourceMesh: any = materialOpened.snapshot.meshes.find((candidate: any) => candidate.id === meshId);
if (!sourceMaterial || !sourceMesh) throw new Error("material fixture is incomplete");
const createdUV = await materialClient.applyCommand({ type: "createUVMap", meshId, name: "WBV2P3001SecondaryUV", baseRevision: materialOpened.snapshot.revision } as any);
const activeUV = await materialClient.applyCommand({ type: "setActiveUVMap", meshId, name: "WBV2P3001SecondaryUV", baseRevision: createdUV.snapshot.revision } as any);
const unwrapped = await materialClient.applyCommand({ type: "unwrapUV", meshId, faceIndices: [0, 1, 2, 3, 4, 5], method: "CUBE", baseRevision: activeUV.snapshot.revision } as any);
const editedMaterial = await materialClient.applyCommand({ type: "setMaterialPrincipled", materialId, baseColor: [0.2, 0.4, 0.6, 1], roughness: 0.31, metallic: 0.72, ior: 1.5, alpha: 1, baseRevision: unwrapped.snapshot.revision } as any);
const invalidUV = await materialClient.applyCommand({ type: "setActiveUVMap", meshId, name: "missing-uv", baseRevision: editedMaterial.snapshot.revision } as any).then(() => ({ code: "NO_ERROR" })).catch(shape);
const materialAfterFailure = await materialClient.snapshot();
const materialSaved = await materialClient.saveBlend();
const materialSavedSha256 = await digestBytes(materialSaved.slice(0));
const materialReopened = await materialReopenedClient.openBlend(materialSaved.slice(0));
const material = materialReopened.snapshot.materials.find((candidate: any) => candidate.id === materialId);
const materialMesh = materialReopened.snapshot.meshes.find((candidate: any) => candidate.id === meshId);
const animationOpened = await animationClient.openBlend(buffer(input.animationBytes));
const animationObject: any = animationOpened.snapshot.nodes.find((candidate: any) => candidate.id === "object:WBV2P3002AnimatedCube");
const animation: any = animationOpened.snapshot.animations.find((candidate: any) => candidate.targetId === animationObject?.id);
if (!animationObject || !animation) throw new Error("animation fixture is incomplete");
const inserted = await animationClient.applyCommand({ type: "insertObjectKeyframe", objectId: animationObject.id, frame: 25, property: "LOCATION", interpolation: "BEZIER", baseRevision: animationOpened.snapshot.revision } as any);
const interpolated = await animationClient.applyCommand({ type: "setFCurveInterpolation", animationId: animation.id, path: "location[0]", interpolation: "LINEAR", baseRevision: inserted.snapshot.revision } as any);
const staleFrame = await animationClient.applyCommand({ type: "insertObjectKeyframe", objectId: animationObject.id, frame: 30, property: "ROTATION_EULER", interpolation: "BEZIER", baseRevision: animationOpened.snapshot.revision } as any).then(() => ({ code: "NO_ERROR" })).catch(shape);
const animationAfterFailure = await animationClient.snapshot();
const animationSaved = await animationClient.saveBlend();
const animationSavedSha256 = await digestBytes(animationSaved.slice(0));
const animationReopened = await animationReopenedClient.openBlend(animationSaved.slice(0));
const reopenedAnimation: any = animationReopened.snapshot.animations.find((candidate: any) => candidate.id === animation.id);
return {
material: { sourceUnsupportedNode: sourceMaterial.nodes?.some((node: any) => node.type === "UNSUPPORTED") ?? false, baseRevision: materialOpened.snapshot.revision, finalRevision: editedMaterial.snapshot.revision, materialId, meshId, activeUVMap: materialMesh?.activeUVMap, baseColor: material?.baseColor, roughness: material?.roughness, metallic: material?.metallic, invalidUV, revisionAfterFailure: materialAfterFailure.snapshot.revision, savedByteLength: materialSaved.byteLength, sourceSha256: await digestBytes(buffer(input.materialBytes)), saveReopenExact: material?.baseColor?.join(",") === "0.20000000298023224,0.4000000059604645,0.6000000238418579,1" && materialMesh?.activeUVMap === "WBV2P3001SecondaryUV" },
animation: { baseRevision: animationOpened.snapshot.revision, insertedRevision: inserted.snapshot.revision, finalRevision: interpolated.snapshot.revision, animationId: animation.id, channelCount: animation.channels.length, locationFrames: interpolated.snapshot.animations.find((candidate: any) => candidate.id === animation.id)?.channels.find((channel: any) => channel.path === "location[0]")?.keyframes.map((keyframe: any) => keyframe.frame), interpolation: interpolated.snapshot.animations.find((candidate: any) => candidate.id === animation.id)?.channels.find((channel: any) => channel.path === "location[0]")?.keyframes.at(-1)?.interpolation, staleFrame, revisionAfterFailure: animationAfterFailure.snapshot.revision, savedByteLength: animationSaved.byteLength, sourceSha256: await digestBytes(buffer(input.animationBytes)), reopenedFrames: reopenedAnimation?.channels.find((channel: any) => channel.path === "location[0]")?.keyframes.map((keyframe: any) => keyframe.frame), reopenedInterpolation: reopenedAnimation?.channels.find((channel: any) => channel.path === "location[0]")?.keyframes.at(-1)?.interpolation, saveReopenExact: reopenedAnimation?.channels.find((channel: any) => channel.path === "location[0]")?.keyframes.at(-1)?.interpolation === "LINEAR" },
};
}
finally { materialClient.terminate(); materialReopenedClient.terminate(); animationClient.terminate(); animationReopenedClient.terminate(); }
}, { materialBytes: Array.from(materialFixture), animationBytes: Array.from(animationFixture) });
fs.mkdirSync(evidenceRoot, { recursive: true });
const output = process.env.WBV2_MATERIAL_ANIMATION_REPORT ?? "material-animation-production.json";
fs.writeFileSync(path.join(evidenceRoot, output), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P12-003", operation: "MATERIAL_ANIMATION_ENGINE_CHROMIUM", browser: "chromium", queueMutation: false, ...result }, null, 2)}\n`);
expect(result.material.sourceUnsupportedNode).toBe(true);
expect(result.material.finalRevision).toBe(result.material.baseRevision + 4);
expect(result.material.activeUVMap).toBe("WBV2P3001SecondaryUV");
expect(result.material.baseColor[0]).toBeCloseTo(0.2, 5);
expect(result.material.roughness).toBeCloseTo(0.31, 5);
expect(result.material.metallic).toBeCloseTo(0.72, 5);
expect(result.material.invalidUV.code).not.toBe("NO_ERROR");
expect(result.material.revisionAfterFailure).toBe(result.material.finalRevision);
expect(result.material.saveReopenExact).toBe(true);
expect(result.material.savedSha256).not.toBe(result.material.sourceSha256);
expect(result.animation.insertedRevision).toBe(result.animation.baseRevision + 1);
expect(result.animation.finalRevision).toBe(result.animation.baseRevision + 2);
expect(result.animation.locationFrames).toEqual([1, 10, 20, 25]);
expect(result.animation.interpolation).toBe("LINEAR");
expect(result.animation.staleFrame.code).not.toBe("NO_ERROR");
expect(result.animation.revisionAfterFailure).toBe(result.animation.finalRevision);
expect(result.animation.reopenedFrames).toEqual([1, 10, 20, 25]);
expect(result.animation.reopenedInterpolation).toBe("LINEAR");
expect(result.animation.saveReopenExact).toBe(true);
expect(result.animation.savedSha256).not.toBe(result.animation.sourceSha256);
});

View File

@@ -0,0 +1,62 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const evidenceRoot = path.join(root, "tests/golden/WBV2-P12-002");
const fixture = fs.readFileSync(path.join(root, "tests/files/web/generated/WBV2-P2-002-OBJECT.TRANSFORM_HIERARCHY.blend"));
const sourceSha256 = crypto.createHash("sha256").update(fixture).digest("hex");
test("WBV2-P12-002 executes visibility and transform transactions through compiled WASM Main", async ({ page }) => {
test.setTimeout(180_000);
await page.goto("/");
const result = await page.evaluate(async (input) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const client = new WebEngineClient({ timeoutMs: 60_000 });
const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
const shape = (error: any) => ({ code: error?.code, message: error?.message });
const objectState = (snapshot: any, id: string) => {
const object = snapshot.nodes.find((candidate: any) => candidate.id === id);
if (!object) throw new Error(`object missing: ${id}`);
return { id: object.id, visible: object.visible, parentId: object.parentId, translation: object.transform?.translation ?? object.translation, worldMatrix: object.worldMatrix };
};
const digest = async (value: ArrayBuffer): Promise<string> => { const hash = await crypto.subtle.digest("SHA-256", value); return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join(""); };
try {
const opened = await client.openBlend(Uint8Array.from(input.bytes).buffer);
const parentId = "object:WBV2P2002Parent";
const childId = "object:WBV2P2002Child";
const beforeParent = objectState(opened.snapshot, parentId);
const beforeChild = objectState(opened.snapshot, childId);
const hidden = await client.applyCommand({ type: "setObjectVisibility", objectId: parentId, visible: false, baseRevision: opened.snapshot.revision } as any);
const transformed = await client.applyCommand({ type: "setObjectTransform", objectId: childId, translation: [0.5, 2.5, 0], rotationEuler: [0, 0, 0], scale: [1, 1, 1], baseRevision: hidden.snapshot.revision } as any);
const afterParent = objectState(transformed.snapshot, parentId);
const afterChild = objectState(transformed.snapshot, childId);
const stale = await client.applyCommand({ type: "setObjectVisibility", objectId: parentId, visible: true, baseRevision: opened.snapshot.revision } as any).then(() => ({ code: "NO_ERROR" })).catch(shape);
const malformed = await client.applyCommand({ type: "setObjectTransform", objectId: childId, translation: [0.5, 2.5], rotationEuler: [0, 0, 0], scale: [1, 1, 1], baseRevision: transformed.snapshot.revision } as any).then(() => ({ code: "NO_ERROR" })).catch(shape);
const unchanged = (await client.snapshot()).snapshot;
const saved = await client.saveBlend();
const savedSha256 = await digest(saved.slice(0));
const reopened = await reopenedClient.openBlend(saved.slice(0));
const reopenedParent = objectState(reopened.snapshot, parentId);
const reopenedChild = objectState(reopened.snapshot, childId);
return { baseRevision: opened.snapshot.revision, hiddenRevision: hidden.snapshot.revision, transformedRevision: transformed.snapshot.revision, beforeParent, beforeChild, afterParent, afterChild, stale, malformed, unchangedParent: objectState(unchanged, parentId), unchangedChild: objectState(unchanged, childId), reopenedParent, reopenedChild, savedSha256, sourceSha256: await digest(Uint8Array.from(input.bytes).buffer), exactSaveReopen: JSON.stringify({ parent: afterParent, child: afterChild }) === JSON.stringify({ parent: reopenedParent, child: reopenedChild }) };
}
finally { client.terminate(); reopenedClient.terminate(); }
}, { bytes: Array.from(fixture) });
fs.mkdirSync(evidenceRoot, { recursive: true });
const output = process.env.WBV2_SCENE_OBJECT_REPORT ?? "scene-object-production.json";
fs.writeFileSync(path.join(evidenceRoot, output), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P12-002", operation: "SCENE_OBJECT_WORKFLOW_ENGINE_CHROMIUM", browser: "chromium", queueMutation: false, ...result }, null, 2)}\n`);
expect(result.sourceSha256).toBe(sourceSha256);
expect(result.hiddenRevision).toBe(result.baseRevision + 1);
expect(result.transformedRevision).toBe(result.baseRevision + 2);
expect(result.afterParent.visible).toBe(false);
expect(result.afterChild.translation).toEqual([0.5, 2.5, 0]);
expect(result.exactSaveReopen).toBe(true);
expect(result.stale.code).not.toBe("NO_ERROR");
expect(result.stale.message).toContain("REVISION_CONFLICT");
expect(result.malformed.code).not.toBe("NO_ERROR");
expect(result.unchangedParent).toEqual(result.afterParent);
expect(result.unchangedChild).toEqual(result.afterChild);
expect(result.savedSha256).not.toBe(sourceSha256);
});

View File

@@ -0,0 +1,83 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const evidenceRoot = path.join(root, "tests/golden/WBV2-P12-004");
const viewportFixture = fs.readFileSync(path.join(root, "tests/files/web/generated/WBV2-P2-004-VIEWPORT.MAIN_OFFSCREEN.blend"));
const nonMeshFixture = fs.readFileSync(path.join(root, "tests/files/web/generated/WBV2-P4-003-SCULPT.NONMESH.PAINT_BOUNDED.blend"));
const digest = (bytes: Uint8Array): string => crypto.createHash("sha256").update(bytes).digest("hex");
test("WBV2-P12-004 binds viewport pixels and non-mesh boundaries to compiled WASM Main", async ({ page }) => {
test.setTimeout(240_000);
await page.goto("/");
const result = await page.evaluate(async (input) => {
const [{ WebEngineClient }, { ViewportRenderer }] = await Promise.all([
import("/src/engine-client/WebEngineClient.ts"),
import("/src/three-adapter/viewport.ts"),
]);
const buffer = (values: number[]): ArrayBuffer => Uint8Array.from(values).buffer;
const digestBytes = async (value: ArrayBuffer): Promise<string> => { const hash = await crypto.subtle.digest("SHA-256", value); return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join(""); };
const viewportClient = new WebEngineClient({ timeoutMs: 60_000 });
const nonMeshClient = new WebEngineClient({ timeoutMs: 60_000 });
const nonMeshReopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
const canvas = document.createElement("canvas");
canvas.width = 320;
canvas.height = 240;
canvas.style.cssText = "position:fixed;left:0;top:0;width:320px;height:240px";
document.body.append(canvas);
let renderer: any;
try {
const viewport = await viewportClient.openBlend(buffer(input.viewportBytes));
renderer = new ViewportRenderer(canvas);
renderer.setSnapshot(viewport.snapshot, viewport.geometryBuffers, viewport.nonMeshGeometryBuffers ?? []);
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const gl = renderer.renderer.getContext();
const pixels = new Uint8Array(32 * 32 * 4);
gl.readPixels(0, 0, 32, 32, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
const readbackPixels = Array.from({ length: 32 * 32 }, (_, index) => pixels[index * 4] + pixels[index * 4 + 1] + pixels[index * 4 + 2] > 30).filter(Boolean).length;
const viewportResources = await viewportClient.openResourceStatus();
const opened = await nonMeshClient.openBlend(buffer(input.nonMeshBytes));
const data = opened.snapshot.nonMeshData ?? [];
const curve = data.find((entry: any) => entry.type === "CURVE");
if (!curve) throw new Error("non-mesh fixture has no Curve data");
const gates = data.map((entry: any) => ({ id: entry.id, type: entry.type, status: nonMeshClient.queryNonMeshCapability(entry.id) }));
const resolvedGates = [];
for (const entry of gates) resolvedGates.push({ id: entry.id, type: entry.type, gate: await entry.status });
const curveControlPoints = [0, 0, 0, 1, 0, 0, 2, 0, 0];
const curveSplineOffsets = [0, 3];
const editedCurve = await nonMeshClient.applyCommand({ type: "setCurveControlPoints", dataId: curve.id, controlPoints: curveControlPoints, splineOffsets: curveSplineOffsets, resolution: curve.resolution ?? 12, baseRevision: opened.snapshot.revision } as any);
const unknown = await nonMeshClient.queryNonMeshCapability("nonmesh:missing");
const saved = await nonMeshClient.saveBlend();
const savedSha256 = await digestBytes(saved.slice(0));
const reopened = await nonMeshReopenedClient.openBlend(saved.slice(0));
const reopenedCurve = reopened.snapshot.nonMeshData?.find((entry: any) => entry.id === curve.id);
const malformed = await nonMeshClient.applyCommand({ type: "setCurveControlPoints", dataId: curve.id, controlPoints: [0, 1], splineOffsets: [0, 1], resolution: curve.resolution, baseRevision: editedCurve.snapshot.revision } as any).then(() => ({ code: "NO_ERROR" })).catch((error: any) => ({ code: error?.code, message: error?.message }));
const afterMalformed = (await nonMeshClient.snapshot()).snapshot;
return { viewport: { revision: viewport.snapshot.revision, sceneId: viewport.snapshot.sceneId, meshCount: viewport.snapshot.meshes.length, geometryBuffers: viewport.geometryBuffers.length, readbackPixels, resources: viewportResources }, nonMesh: { baseRevision: opened.snapshot.revision, editedRevision: editedCurve.snapshot.revision, data: data.map((entry: any) => ({ id: entry.id, type: entry.type, geometryStatus: entry.geometryStatus, pointCount: entry.pointCount, splineCount: entry.splineCount })), gates: resolvedGates.map((entry: any) => ({ id: entry.id, type: entry.type, status: entry.gate.status, capability: entry.gate.capability, issueCodes: entry.gate.issues.map((issue: any) => issue.code) })), unknown: { status: unknown.status, capability: unknown.capability, issueCode: unknown.issues[0]?.code }, curveId: curve.id, curveBefore: { controlPoints: curve.controlPoints, pointCount: curve.pointCount }, curveAfter: { controlPoints: editedCurve.snapshot.nonMeshData?.find((entry: any) => entry.id === curve.id)?.controlPoints, pointCount: editedCurve.snapshot.nonMeshData?.find((entry: any) => entry.id === curve.id)?.pointCount }, reopenedCurve: { controlPoints: reopenedCurve?.controlPoints, pointCount: reopenedCurve?.pointCount }, malformed, revisionAfterMalformed: afterMalformed.revision, savedSha256, sourceSha256: await digestBytes(buffer(input.nonMeshBytes)), saveReopenExact: JSON.stringify(editedCurve.snapshot.nonMeshData?.find((entry: any) => entry.id === curve.id)) === JSON.stringify(reopenedCurve) } };
}
catch (error: any) { return { error: { code: error?.code, message: error?.message, detail: String(error) } } as any; }
finally { renderer?.dispose(); canvas.remove(); viewportClient.terminate(); nonMeshClient.terminate(); nonMeshReopenedClient.terminate(); }
}, { viewportBytes: Array.from(viewportFixture), nonMeshBytes: Array.from(nonMeshFixture) });
fs.mkdirSync(evidenceRoot, { recursive: true });
const output = process.env.WBV2_VIEWPORT_NONMESH_REPORT ?? "viewport-nonmesh-production.json";
fs.writeFileSync(path.join(evidenceRoot, output), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P12-004", operation: "VIEWPORT_NONMESH_ENGINE_CHROMIUM", browser: "chromium", queueMutation: false, ...result }, null, 2)}\n`);
expect(result.error).toBeUndefined();
expect(result.viewport.sceneId).toBe("scene:WBV2P2004ViewportScene");
expect(result.viewport.meshCount).toBeGreaterThan(0);
expect(result.viewport.geometryBuffers).toBeGreaterThan(0);
expect(result.viewport.readbackPixels).toBeGreaterThan(0);
expect(result.viewport.resources).toMatchObject({ activeRequests: 0, liveInputBytes: 0, liveStagingFiles: 0 });
expect(result.nonMesh.data.length).toBe(2);
expect(result.nonMesh.data.map((entry) => entry.type)).toEqual(["CURVE", "POINT_CLOUD"]);
expect(result.nonMesh.gates.some((gate) => gate.status === "READY")).toBe(true);
expect(result.nonMesh.unknown.status).toBe("BLOCKED");
expect(result.nonMesh.unknown.issueCode).toBe("NON_MESH_DATA_UNSUPPORTED");
expect(result.nonMesh.editedRevision).toBe(result.nonMesh.baseRevision + 1);
expect(result.nonMesh.saveReopenExact).toBe(true);
expect(result.nonMesh.malformed.code).not.toBe("NO_ERROR");
expect(result.nonMesh.revisionAfterMalformed).toBe(result.nonMesh.editedRevision);
expect(result.nonMesh.savedSha256).not.toBe(result.nonMesh.sourceSha256);
});