120 lines
5.6 KiB
JavaScript
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);
|
|
}
|