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); }