Files
workinf_Blender_Wasm/tools/web/check-nla-evaluation-golden.mjs
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

120 lines
5.6 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-11/nla-evaluation.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(crypto.createHash("sha256").update(fixture).digest("hex"), golden.fixtureSha256);
assert.equal(golden.schemaVersion, 1);
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 setFrame(engine, handle, frame) {
const command = new TextEncoder().encode(JSON.stringify({ type: "setFrame", frame }));
const pointer = engine._malloc(command.byteLength);
try {
engine.HEAPU8.set(command, pointer);
assert.equal(engine._web_engine_apply_command(handle, pointer, command.byteLength), 0,
engine.UTF8ToString(engine._web_engine_last_error_message()));
}
finally { engine._free(pointer); }
}
function assertClose(expected, actual, label) {
assert.equal(actual.length, expected.length, `${label} length`);
const maximum = Math.max(0, ...expected.map((value, index) => Math.abs(actual[index] - value)));
assert.ok(maximum <= golden.tolerance.maxMatrixError, `${label} max error ${maximum}`);
}
function assertNlaSnapshot(snapshot) {
assert.equal(snapshot.nlaTracks?.length, golden.tracks.length, "NLA track count");
const track = snapshot.nlaTracks[0];
const expectedTrack = golden.tracks[0];
assert.equal(track.schemaVersion, 1);
assert.equal(track.ownerId, `object:${golden.object}`);
assert.equal(track.name, expectedTrack.name);
assert.equal(track.muted, expectedTrack.muted);
assert.equal(track.solo, expectedTrack.solo);
assert.equal(track.strips.length, expectedTrack.strips.length);
expectedTrack.strips.forEach((expected, index) => {
const actual = track.strips[index];
assert.equal(actual.id, expected.id);
assert.equal(actual.actionId, `action:${expected.action}:object:${golden.object}`);
for (const field of ["frameStart", "frameEnd", "actionFrameStart", "actionFrameEnd", "scale", "repeat", "blendIn", "blendOut", "influence"]) {
assert.equal(actual[field], expected[field], `${expected.id}.${field}`);
}
for (const field of ["blendMode", "extrapolation", "muted", "reverse"]) {
assert.equal(actual[field], expected[field], `${expected.id}.${field}`);
}
assert.equal(actual.stripType, "CLIP");
assert.equal(actual.useTimeWarp, false);
});
for (const expected of expectedTrack.strips) {
const action = snapshot.animations.find((candidate) => candidate.id === `action:${expected.action}:object:${golden.object}`);
assert.ok(action, `${expected.action} animation`);
assert.equal(action.targetId, `object:${golden.object}`);
assert.equal(action.frameStart, 1);
assert.equal(action.frameEnd, 11);
assert.ok(action.channels.some((channel) => channel.path.startsWith("location[")), `${expected.action} location channel`);
}
}
const engine = await factory({ wasmBinary: wasmBinary.slice() });
const handle = engine._web_engine_create();
assert.ok(handle > 0);
try {
open(engine, handle, fixture);
const initial = output(engine, handle, engine._web_engine_get_scene_snapshot);
assertNlaSnapshot(initial);
const initialNla = JSON.stringify({ nlaTracks: initial.nlaTracks, animations: initial.animations });
let maximumMatrixError = 0;
for (const expected of golden.frames) {
setFrame(engine, handle, expected.frame);
const report = output(engine, handle, engine._web_engine_evaluate_depsgraph);
assert.equal(report.engine, "BlenderDepsgraph");
assert.equal(report.status, "EVALUATED");
assert.equal(report.frame, expected.frame);
const mesh = report.meshes.find((candidate) => candidate.objectId === `object:${golden.object}`);
assert.ok(mesh, `evaluated ${golden.object} at frame ${expected.frame}`);
const errors = mesh.worldMatrix.map((value, index) => Math.abs(value - expected.worldMatrix[index]));
maximumMatrixError = Math.max(maximumMatrixError, ...errors);
assertClose(expected.worldMatrix, mesh.worldMatrix, `frame ${expected.frame}`);
const after = output(engine, handle, engine._web_engine_get_scene_snapshot);
assert.equal(JSON.stringify({ nlaTracks: after.nlaTracks, animations: after.animations }), initialNla,
`NLA read-only identity changed at frame ${expected.frame}`);
}
process.stdout.write(`nla-evaluation-ok tracks=${golden.tracks.length} strips=${golden.tracks[0].strips.length} frames=${golden.frames.length} max-matrix-error=${maximumMatrixError} read-only=passed\n`);
}
finally {
engine._web_engine_destroy(handle);
}