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

141 lines
6.4 KiB
JavaScript

import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import factory from "../../web/app/src/vendor/blender/web_engine.js";
const root = new URL("../../", import.meta.url);
const golden = JSON.parse(fs.readFileSync(new URL("tests/golden/M10-03/geometry-node-evaluator.json", root), "utf8"));
const fixture = fs.readFileSync(new URL(golden.fixture, root));
const wasmBinary = fs.readFileSync(new URL("web/app/src/vendor/blender/web_engine.wasm", root));
assert.equal(golden.schemaVersion, 1);
assert.equal(golden.blenderVersion, "5.2.0 LTS");
assert.equal(crypto.createHash("sha256").update(fixture).digest("hex"), golden.fixtureSha256);
assert.equal(golden.allowlist.length, 16);
assert.deepEqual(Object.keys(golden.nodeCoverage).sort(), [...golden.allowlist].sort());
for (const nodeType of golden.allowlist) {
assert.ok(golden.nodeCoverage[nodeType].length > 0, `${nodeType} has no desktop fixture coverage`);
}
function open(engine, handle, bytes) {
const pointer = engine._malloc(bytes.byteLength);
try {
engine.HEAPU8.set(bytes, pointer);
assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.byteLength), 0,
engine.UTF8ToString(engine._web_engine_last_error_message()));
}
finally {
engine._free(pointer);
}
}
function output(engine, handle, fn) {
const dataOut = engine._malloc(4);
const lengthOut = engine._malloc(4);
try {
assert.equal(fn(handle, dataOut, lengthOut), 0,
engine.UTF8ToString(engine._web_engine_last_error_message()));
const pointer = engine.HEAPU32[dataOut >>> 2];
const length = engine.HEAPU32[lengthOut >>> 2];
assert.ok(pointer > 0 && length > 0);
return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length)));
}
finally {
engine._free(dataOut);
engine._free(lengthOut);
}
}
function errors(expected, actual) {
assert.equal(actual.length, expected.length);
return actual.map((value, index) => value - expected[index]);
}
function compareFloatArray(expected, actual, maximum, rmsMaximum, label) {
const delta = errors(expected, actual);
const maxError = Math.max(0, ...delta.map((value) => Math.abs(value)));
const rmsError = delta.length === 0 ? 0 : Math.sqrt(
delta.reduce((sum, value) => sum + value * value, 0) / delta.length,
);
assert.ok(maxError <= maximum, `${label} max error ${maxError} exceeds ${maximum}`);
assert.ok(rmsError <= rmsMaximum, `${label} RMS error ${rmsError} exceeds ${rmsMaximum}`);
return { maxError, rmsError };
}
function bounds(positions) {
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]);
}
}
if (positions.length === 0) return { min: [0, 0, 0], max: [0, 0, 0] };
return result;
}
const engine = await factory({ wasmBinary: wasmBinary.slice() });
const handle = engine._web_engine_create();
assert.ok(handle > 0);
try {
open(engine, handle, fixture);
const snapshot = output(engine, handle, engine._web_engine_get_scene_snapshot);
const report = output(engine, handle, engine._web_engine_evaluate_depsgraph);
assert.equal(report.engine, "BlenderDepsgraph");
assert.equal(report.status, "EVALUATED");
const graphs = new Map((snapshot.geometryNodeGraphs ?? []).map((graph) => [graph.name, graph]));
const meshes = new Map(report.meshes.map((mesh) => [mesh.objectId, mesh]));
let maximumPositionError = 0;
let maximumRmsError = 0;
for (const expectedCase of golden.cases) {
const graph = graphs.get(expectedCase.graph);
assert.ok(graph, `${expectedCase.name} graph is missing from the Main snapshot`);
assert.deepEqual(graph.nodes.map((node) => node.type).sort(), [...expectedCase.nodeTypes].sort(),
`${expectedCase.name} node inventory`);
const actual = meshes.get(`object:${expectedCase.name}`);
assert.ok(actual, `${expectedCase.name} evaluated mesh is missing`);
assert.equal(actual.vertexCount, expectedCase.mesh.vertexCount, `${expectedCase.name} vertex count`);
assert.equal(actual.triangleCount, expectedCase.mesh.triangleCount, `${expectedCase.name} triangle count`);
assert.deepEqual(actual.indices, expectedCase.mesh.indices, `${expectedCase.name} topology`);
assert.equal(actual.modifiers.length, 1, `${expectedCase.name} modifier count`);
assert.equal(actual.modifiers[0].status, "EVALUATED", `${expectedCase.name} modifier status`);
const positionError = compareFloatArray(
expectedCase.mesh.positions,
actual.positions,
golden.tolerance.maxPositionError,
golden.tolerance.rmsPositionError,
`${expectedCase.name} positions`,
);
maximumPositionError = Math.max(maximumPositionError, positionError.maxError);
maximumRmsError = Math.max(maximumRmsError, positionError.rmsError);
const actualBounds = bounds(actual.positions);
compareFloatArray(expectedCase.mesh.bounds.min, actualBounds.min,
golden.tolerance.boundsError, golden.tolerance.boundsError, `${expectedCase.name} minimum bounds`);
compareFloatArray(expectedCase.mesh.bounds.max, actualBounds.max,
golden.tolerance.boundsError, golden.tolerance.boundsError, `${expectedCase.name} maximum bounds`);
assert.deepEqual(Object.keys(actual.attributes ?? {}).sort(),
Object.keys(expectedCase.mesh.attributes).sort(), `${expectedCase.name} attribute names`);
for (const [name, expectedAttribute] of Object.entries(expectedCase.mesh.attributes)) {
const actualAttribute = actual.attributes[name];
assert.equal(actualAttribute.domain, expectedAttribute.domain, `${expectedCase.name}/${name} domain`);
assert.equal(actualAttribute.dataType, expectedAttribute.dataType, `${expectedCase.name}/${name} data type`);
compareFloatArray(expectedAttribute.values, actualAttribute.values,
golden.tolerance.maxAttributeError, golden.tolerance.maxAttributeError,
`${expectedCase.name}/${name} values`);
}
}
process.stdout.write(
`geometry-node-evaluator-ok nodes=${golden.allowlist.length} cases=${golden.cases.length} ` +
`max-position-error=${maximumPositionError} max-rms-error=${maximumRmsError} attributes=passed\n`,
);
}
finally {
engine._web_engine_destroy(handle);
}