Files
workinf_Blender_Wasm/web/tests/e2e/geometry-node-evaluator-golden.spec.ts
mes123456 0fe8d2bb56
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
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

165 lines
6.6 KiB
TypeScript

import { expect, test } from "@playwright/test";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
interface GoldenMesh {
attributes: Record<string, {
domain: "POINT";
dataType: "FLOAT";
values: number[];
}>;
bounds: { min: number[]; max: number[] };
indices: number[];
positions: number[];
triangleCount: number;
vertexCount: number;
}
interface GoldenCase {
graph: string;
mesh: GoldenMesh;
name: string;
nodeTypes: string[];
}
interface Golden {
allowlist: string[];
cases: GoldenCase[];
fixture: string;
fixtureSha256: string;
nodeCoverage: Record<string, string[]>;
schemaVersion: number;
tolerance: {
boundsError: number;
maxAttributeError: number;
maxPositionError: number;
rmsPositionError: number;
};
}
interface EvaluatedMesh {
attributes?: GoldenMesh["attributes"];
indices: number[];
modifiers: Array<{ status: string }>;
objectId: string;
positions: number[];
triangleCount: number;
vertexCount: number;
}
interface EvaluationResult {
graphs: Array<{ name: string; nodes: Array<{ type: string }> }>;
report: { engine: string; status: string; meshes: EvaluatedMesh[] };
}
const root = path.resolve(import.meta.dirname, "../../..");
const golden = JSON.parse(fs.readFileSync(
path.join(root, "tests/golden/M10-03/geometry-node-evaluator.json"),
"utf8",
)) as Golden;
const blendBytes = fs.readFileSync(path.join(root, golden.fixture));
function errorMetrics(expected: number[], actual: number[]) {
expect(actual).toHaveLength(expected.length);
const errors = actual.map((value, index) => value - expected[index]);
return {
maximum: Math.max(0, ...errors.map((value) => Math.abs(value))),
rms: errors.length === 0 ? 0 : Math.sqrt(
errors.reduce((sum, value) => sum + value * value, 0) / errors.length,
),
};
}
function bounds(positions: number[]) {
if (positions.length === 0) return { min: [0, 0, 0], max: [0, 0, 0] };
const result = { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] };
for (let index = 0; index < positions.length; index += 3) {
for (let axis = 0; axis < 3; axis++) {
result.min[axis] = Math.min(result.min[axis], positions[index + axis]);
result.max[axis] = Math.max(result.max[axis], positions[index + axis]);
}
}
return result;
}
function verifyEvaluation(result: EvaluationResult) {
expect(result.report.engine).toBe("BlenderDepsgraph");
expect(result.report.status).toBe("EVALUATED");
const graphs = new Map(result.graphs.map((graph) => [graph.name, graph]));
const meshes = new Map(result.report.meshes.map((mesh) => [mesh.objectId, mesh]));
for (const expectedCase of golden.cases) {
const graph = graphs.get(expectedCase.graph);
expect(graph, `${expectedCase.name} graph`).toBeDefined();
expect(graph?.nodes.map((node) => node.type).sort(), `${expectedCase.name} node inventory`)
.toEqual([...expectedCase.nodeTypes].sort());
const actual = meshes.get(`object:${expectedCase.name}`);
expect(actual, `${expectedCase.name} evaluated mesh`).toBeDefined();
if (!actual) continue;
expect(actual.vertexCount, `${expectedCase.name} vertex count`).toBe(expectedCase.mesh.vertexCount);
expect(actual.triangleCount, `${expectedCase.name} triangle count`).toBe(expectedCase.mesh.triangleCount);
expect(actual.indices, `${expectedCase.name} topology`).toEqual(expectedCase.mesh.indices);
expect(actual.modifiers).toHaveLength(1);
expect(actual.modifiers[0].status, `${expectedCase.name} modifier status`).toBe("EVALUATED");
const positionError = errorMetrics(expectedCase.mesh.positions, actual.positions);
expect(positionError.maximum, `${expectedCase.name} maximum position error`)
.toBeLessThanOrEqual(golden.tolerance.maxPositionError);
expect(positionError.rms, `${expectedCase.name} RMS position error`)
.toBeLessThanOrEqual(golden.tolerance.rmsPositionError);
const actualBounds = bounds(actual.positions);
expect(errorMetrics(expectedCase.mesh.bounds.min, actualBounds.min).maximum,
`${expectedCase.name} minimum bounds error`).toBeLessThanOrEqual(golden.tolerance.boundsError);
expect(errorMetrics(expectedCase.mesh.bounds.max, actualBounds.max).maximum,
`${expectedCase.name} maximum bounds error`).toBeLessThanOrEqual(golden.tolerance.boundsError);
expect(Object.keys(actual.attributes ?? {}).sort(), `${expectedCase.name} attribute inventory`)
.toEqual(Object.keys(expectedCase.mesh.attributes).sort());
for (const [name, expectedAttribute] of Object.entries(expectedCase.mesh.attributes)) {
const actualAttribute = actual.attributes?.[name];
expect(actualAttribute, `${expectedCase.name}/${name}`).toBeDefined();
expect(actualAttribute?.domain).toBe(expectedAttribute.domain);
expect(actualAttribute?.dataType).toBe(expectedAttribute.dataType);
expect(errorMetrics(expectedAttribute.values, actualAttribute?.values ?? []).maximum,
`${expectedCase.name}/${name} value error`)
.toBeLessThanOrEqual(golden.tolerance.maxAttributeError);
}
}
}
test("M10-03 matches every allowlisted Geometry Node against Blender 5.2 desktop goldens", async ({ page }) => {
test.setTimeout(180_000);
expect(golden.schemaVersion).toBe(1);
expect(crypto.createHash("sha256").update(blendBytes).digest("hex")).toBe(golden.fixtureSha256);
expect(Object.keys(golden.nodeCoverage).sort()).toEqual([...golden.allowlist].sort());
expect(golden.allowlist).toHaveLength(16);
await page.goto("/");
const evaluations = await page.evaluate(async ({ bytes }) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
const first = new WebEngineClient({ timeoutMs: 90_000 });
await first.init();
const opened = await first.openBlend(source);
const firstReport = await first.evaluateDepsgraph();
const saved = await first.saveBlend();
const initial = { graphs: opened.snapshot.geometryNodeGraphs ?? [], report: firstReport.depsgraph };
first.terminate();
const second = new WebEngineClient({ timeoutMs: 90_000 });
await second.init();
const reopened = await second.openBlend(saved);
const secondReport = await second.evaluateDepsgraph();
const restored = { graphs: reopened.snapshot.geometryNodeGraphs ?? [], report: secondReport.depsgraph };
second.terminate();
return [initial, restored];
}, { bytes: new Uint8Array(blendBytes) }) as EvaluationResult[];
expect(evaluations).toHaveLength(2);
verifyEvaluation(evaluations[0]);
verifyEvaluation(evaluations[1]);
});