import assert from "node:assert/strict"; import fs from "node:fs"; import factory from "../../web/app/src/vendor/blender/web_engine.js"; const root = new URL("../../", import.meta.url); const wasmBinary = fs.readFileSync(new URL("web/app/src/vendor/blender/web_engine.wasm", root)); const cases = [ { golden: JSON.parse(fs.readFileSync(new URL("tests/golden/W-080/animation-depsgraph.json", root), "utf8")), samplesKey: "frames", }, { golden: JSON.parse(fs.readFileSync(new URL("tests/golden/W-080/pose-constraint-depsgraph.json", root), "utf8")), samplesKey: "samples", }, ]; 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 command(engine, handle, payload) { const bytes = new TextEncoder().encode(JSON.stringify(payload)); const pointer = engine._malloc(bytes.byteLength); try { engine.HEAPU8.set(bytes, pointer); assert.equal( engine._web_engine_apply_command(handle, pointer, bytes.byteLength), 0, engine.UTF8ToString(engine._web_engine_last_error_message()), ); } finally { engine._free(pointer); } } function readOutput(engine, handle, fn, owned) { 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 && length, "native response is empty"); const bytes = engine.HEAPU8.slice(pointer, pointer + length); if (owned) engine._web_engine_free_buffer(pointer); return bytes; } finally { engine._free(dataOut); engine._free(lengthOut); } } function evaluate(engine, handle) { const bytes = readOutput(engine, handle, engine._web_engine_evaluate_depsgraph, false); return JSON.parse(new TextDecoder().decode(bytes)); } function save(engine, handle) { return readOutput(engine, handle, engine._web_engine_save_blend, true); } function maxError(actual, expected) { assert.equal(actual.length, expected.length); return Math.max(0, ...actual.map((value, index) => Math.abs(value - expected[index]))); } for (const testCase of cases) { const { golden } = testCase; const source = fs.readFileSync(new URL(`tests/files/web/${golden.fixture}`, root)); const engine = await factory({ wasmBinary: wasmBinary.slice() }); const sourceHandle = engine._web_engine_create(); open(engine, sourceHandle, source); command(engine, sourceHandle, { type: "setFrame", frame: 5 }); const firstGeneration = save(engine, sourceHandle); engine._web_engine_destroy(sourceHandle); const reopenedHandle = engine._web_engine_create(); open(engine, reopenedHandle, firstGeneration); const samples = new Map(golden[testCase.samplesKey].map((sample) => [sample.frame, sample])); const frames = testCase.samplesKey === "frames" ? golden.frames.map((sample) => sample.frame) : golden.frames; for (const frame of frames) { command(engine, reopenedHandle, { type: "setFrame", frame }); const report = evaluate(engine, reopenedHandle); const expected = samples.get(frame); assert.ok(expected, `missing golden sample at frame ${frame}`); const mesh = report.meshes.find((candidate) => candidate.sourceMeshId === `mesh:${golden.mesh}`); assert.ok(mesh, `round-trip mesh missing at frame ${frame}`); assert.ok( maxError(mesh.positions, expected.positions) <= golden.tolerance.maxPositionError, `${golden.fixture} frame ${frame} position parity failed`, ); if (expected.worldMatrix) { assert.ok( maxError(mesh.worldMatrix, expected.worldMatrix) <= golden.tolerance.maxMatrixError, `${golden.fixture} frame ${frame} matrix parity failed`, ); } if (expected.bones) { const armature = report.armatures.find((candidate) => candidate.id === `armature:${golden.armature}`); assert.ok(armature, `round-trip armature missing at frame ${frame}`); for (const [boneName, poseMatrix] of Object.entries(expected.bones)) { const bone = armature.bones.find((candidate) => candidate.name === boneName); assert.ok(bone, `${boneName} missing after round-trip at frame ${frame}`); assert.ok( maxError(bone.poseMatrix, poseMatrix) <= golden.tolerance.maxMatrixError, `${boneName} frame ${frame} pose parity failed`, ); } } } const secondGeneration = save(engine, reopenedHandle); engine._web_engine_destroy(reopenedHandle); const secondHandle = engine._web_engine_create(); open(engine, secondHandle, secondGeneration); engine._web_engine_destroy(secondHandle); assert.ok(firstGeneration.byteLength > 0 && secondGeneration.byteLength > 0); } process.stdout.write("main-roundtrip-ok fixtures=animation_scene.blend,pose_constraint_scene.blend frames=1,5,10 generations=2\n");