88 lines
6.5 KiB
TypeScript
88 lines
6.5 KiB
TypeScript
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-P11-001");
|
|
const fixturePath = path.join(root, "tests/files/web/generated/WBV2-P2-003-MESH.EDIT_CORE.blend");
|
|
const fixture = fs.readFileSync(fixturePath);
|
|
const sourceSha256 = crypto.createHash("sha256").update(fixture).digest("hex");
|
|
|
|
test("WBV2-P11-001 executes the six Mesh topology operations 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 operations = [
|
|
{ label: "MERGE", meshId: "mesh:WBV2P2003MergeMesh", selectionMode: "VERT", elementIndices: [0, 1] },
|
|
{ label: "DISSOLVE", meshId: "mesh:WBV2P2003DissolveMesh", selectionMode: "EDGE", elementIndices: [0] },
|
|
{ label: "EXTRUDE", meshId: "mesh:WBV2P2003ExtrudeMesh", selectionMode: "FACE", elementIndices: [0], offset: [0, 0, 1] },
|
|
{ label: "INSET", meshId: "mesh:WBV2P2003InsetMesh", selectionMode: "FACE", elementIndices: [0], amount: 0.2 },
|
|
{ label: "BEVEL", meshId: "mesh:WBV2P2003BevelMesh", selectionMode: "EDGE", elementIndices: [0], amount: 0.1, segments: 2 },
|
|
{ label: "LOOP_CUT", meshId: "mesh:WBV2P2003LoopCutMesh", selectionMode: "EDGE", elementIndices: [0], segments: 1 },
|
|
];
|
|
const summary = (snapshot: any, meshId: string) => {
|
|
const mesh = snapshot.meshes.find((candidate: any) => candidate.id === meshId);
|
|
if (!mesh) throw new Error(`mesh missing: ${meshId}`);
|
|
return { id: mesh.id, vertexCount: mesh.vertexCount, edgeCount: mesh.edgeCount, faceCount: mesh.faceCount, triangleCount: mesh.triangleCount, topology: mesh.topology };
|
|
};
|
|
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(""); };
|
|
const errors = async (client: any, commands: any[], revision: number) => {
|
|
const values = [];
|
|
for (const command of commands) {
|
|
const error = await client.applyCommand({ baseRevision: revision, ...command } as any).then(() => null).catch((caught) => ({ code: caught?.code, message: caught?.message }));
|
|
values.push(error);
|
|
}
|
|
return values;
|
|
};
|
|
const positive = [];
|
|
for (const operation of operations) {
|
|
const client = new WebEngineClient({ timeoutMs: 60_000 });
|
|
const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
|
|
try {
|
|
const opened = await client.openBlend(Uint8Array.from(input.bytes).buffer);
|
|
const before = summary(opened.snapshot, operation.meshId);
|
|
const baseRevision = opened.snapshot.revision;
|
|
const edited = await client.applyCommand({ type: "meshEdit", meshId: operation.meshId, operation: operation.label, selectionMode: operation.selectionMode, elementIndices: operation.elementIndices, ...(operation.offset ? { offset: operation.offset } : {}), ...(operation.amount === undefined ? {} : { amount: operation.amount }), ...(operation.segments === undefined ? {} : { segments: operation.segments }), baseRevision } as any);
|
|
const after = summary(edited.snapshot, operation.meshId);
|
|
const saved = await client.saveBlend();
|
|
const savedSha256 = await digest(saved.slice(0));
|
|
const reopened = await reopenedClient.openBlend(saved.slice(0));
|
|
const reopenedSummary = summary(reopened.snapshot, operation.meshId);
|
|
positive.push({ operation: operation.label, meshId: operation.meshId, baseRevision, afterRevision: edited.snapshot.revision, before, after, reopened: reopenedSummary, savedSha256, exactSaveReopen: JSON.stringify(after) === JSON.stringify(reopenedSummary) });
|
|
}
|
|
finally { client.terminate(); reopenedClient.terminate(); }
|
|
}
|
|
|
|
const negativeClient = new WebEngineClient({ timeoutMs: 60_000 });
|
|
try {
|
|
const opened = await negativeClient.openBlend(Uint8Array.from(input.bytes).buffer);
|
|
const revision = opened.snapshot.revision;
|
|
const before = summary(opened.snapshot, "mesh:WBV2P2003MergeMesh");
|
|
const [invalidSelection, incompatible, stale] = await errors(negativeClient, [
|
|
{ type: "meshEdit", meshId: "mesh:WBV2P2003MergeMesh", operation: "MERGE", selectionMode: "VERT", elementIndices: [999] },
|
|
{ type: "meshEdit", meshId: "mesh:WBV2P2003MergeMesh", operation: "EXTRUDE", selectionMode: "EDGE", elementIndices: [0], offset: [0, 0, 1] },
|
|
{ type: "meshEdit", meshId: "mesh:WBV2P2003MergeMesh", operation: "MERGE", selectionMode: "VERT", elementIndices: [0, 1], baseRevision: revision - 1 },
|
|
], revision);
|
|
const after = summary((await negativeClient.snapshot()).snapshot, "mesh:WBV2P2003MergeMesh");
|
|
return { sourceSha256: input.sourceSha256, operations: positive, negative: { invalidSelection, incompatible, stale, before, after, revisionBefore: revision, revisionAfter: (await negativeClient.snapshot()).snapshot.revision } };
|
|
}
|
|
finally { negativeClient.terminate(); }
|
|
}, { bytes: Array.from(fixture), sourceSha256 });
|
|
|
|
fs.mkdirSync(evidenceRoot, { recursive: true });
|
|
const output = process.env.WBV2_MESH_TOPOLOGY_REPORT ?? "mesh-topology-production.json";
|
|
fs.writeFileSync(path.join(evidenceRoot, output), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P11-001", operation: "MESH_TOPOLOGY_OPERATION_FAMILY_CHROMIUM", browser: "chromium", queueMutation: false, ...result }, null, 2)}\n`);
|
|
expect(result.sourceSha256).toBe(sourceSha256);
|
|
expect(result.operations).toHaveLength(6);
|
|
expect(result.operations.every((entry) => entry.afterRevision === entry.baseRevision + 1 && entry.exactSaveReopen && entry.after.vertexCount > 0 && entry.after.edgeCount > 0 && entry.after.faceCount > 0)).toBe(true);
|
|
expect(result.operations.map((entry) => entry.operation)).toEqual(["MERGE", "DISSOLVE", "EXTRUDE", "INSET", "BEVEL", "LOOP_CUT"]);
|
|
expect(result.negative.invalidSelection?.code).toBe("INVALID_ARGUMENT");
|
|
expect(result.negative.incompatible?.code).toBe("INVALID_ARGUMENT");
|
|
expect(result.negative.stale?.code).toBe("INVALID_ARGUMENT");
|
|
expect(result.negative.stale?.message).toContain("REVISION_CONFLICT");
|
|
expect(result.negative.revisionAfter).toBe(result.negative.revisionBefore);
|
|
expect(result.negative.after).toEqual(result.negative.before);
|
|
});
|