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 golden = JSON.parse(fs.readFileSync(new URL("tests/golden/M9-12/weight-paint.json", root), "utf8")); const fixture = fs.readFileSync(new URL(`tests/files/web/${golden.fixture}`, root)); const wasmBinary = fs.readFileSync(new URL("web/app/src/vendor/blender/web_engine.wasm", root)); function call(engine, handle, name, 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, `${name}: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`); } finally { engine._free(pointer); } } 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 snapshot(engine, handle) { const dataOut = engine._malloc(4); const lengthOut = engine._malloc(4); try { assert.equal(engine._web_engine_get_scene_snapshot(handle, dataOut, lengthOut), 0); const pointer = engine.HEAPU32[dataOut >>> 2]; const length = engine.HEAPU32[lengthOut >>> 2]; return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length))); } finally { engine._free(dataOut); engine._free(lengthOut); } } function save(engine, handle) { const dataOut = engine._malloc(4); const lengthOut = engine._malloc(4); try { assert.equal(engine._web_engine_save_blend(handle, dataOut, lengthOut), 0); const pointer = engine.HEAPU32[dataOut >>> 2]; const length = engine.HEAPU32[lengthOut >>> 2]; const bytes = engine.HEAPU8.slice(pointer, pointer + length); engine._web_engine_free_buffer(pointer); return bytes; } finally { engine._free(dataOut); engine._free(lengthOut); } } function weightsByVertex(snapshotValue) { const mesh = snapshotValue.meshes.find((item) => item.id === `mesh:${golden.mesh}`); assert.ok(mesh?.skinWeights, "weight golden mesh has no skinWeights"); return golden.steps[0].vertices.map((expectedVertex) => { const result = {}; for (const [groupIndex, groupName] of mesh.skinWeights.boneNames.entries()) { const offset = expectedVertex.index * 4; const slot = mesh.skinWeights.indices.slice(offset, offset + 4).findIndex((value) => value === groupIndex); result[groupName] = slot < 0 ? 0 : mesh.skinWeights.weights[offset + slot]; } return result; }); } function compareStep(actualSnapshot, expected, label) { const actual = weightsByVertex(actualSnapshot); assert.deepEqual(actual.map((weights) => Object.keys(weights).sort()), expected.vertices.map(() => expected.groups.slice().sort()), `${label} group schema`); expected.vertices.forEach((vertex, index) => { for (const group of expected.groups) { const error = Math.abs((actual[index][group] ?? 0) - (vertex.weights[group] ?? 0)); assert.ok(error <= golden.tolerance, `${label} vertex=${vertex.index} group=${group} error=${error}`); } }); } const engine = await factory({ wasmBinary: wasmBinary.slice() }); const handle = engine._web_engine_create(); assert.ok(handle > 0); try { open(engine, handle, fixture); call(engine, handle, "initial", { type: "setVertexWeights", objectId: `object:${golden.object}`, vertexGroup: "WebPaintGroup", indices: [0, 1], values: [0.75, 0.25] }); compareStep(snapshot(engine, handle), golden.steps[0], "initial"); call(engine, handle, "normalize", { type: "setVertexWeights", objectId: `object:${golden.object}`, vertexGroup: "WebPaintGroup", indices: [2], values: [0.5], normalize: true }); compareStep(snapshot(engine, handle), golden.steps[1], "normalize"); call(engine, handle, "limit-normalize", { type: "setVertexWeights", objectId: `object:${golden.object}`, vertexGroup: "WebPaintGroup", indices: [3], values: [0.4], limit: 2, normalize: true }); compareStep(snapshot(engine, handle), golden.steps[2], "limit-normalize"); call(engine, handle, "mirror", { type: "setVertexWeights", objectId: `object:${golden.object}`, vertexGroup: "WebPaintGroup", indices: [0], values: [0.9], mirror: true, mirrorAxis: 0, mirrorTolerance: 1e-4 }); compareStep(snapshot(engine, handle), golden.steps[3], "mirror"); const saved = save(engine, handle); const reopened = engine._web_engine_create(); try { open(engine, reopened, saved); compareStep(snapshot(engine, reopened), golden.steps[3], "save-reopen"); } finally { engine._web_engine_destroy(reopened); } } finally { engine._web_engine_destroy(handle); } process.stdout.write(`weight-paint-golden-ok fixture=${golden.fixture} steps=${golden.steps.map((step) => step.name).join(",")} normalize=passed limit=passed mirror=passed save-reopen=passed\n`);