Files
workinf_Blender_Wasm/tools/web/check-paint-roundtrip.mjs
2026-08-12 04:47:48 -04:00

167 lines
7.3 KiB
JavaScript

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 colorFixture = fs.readFileSync(new URL("tests/files/web/attribute_scene.blend", root));
const weightFixture = fs.readFileSync(new URL("tests/files/web/rigged_shape_scene.blend", root));
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, owned = false) {
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];
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 snapshot(engine, handle) {
return JSON.parse(new TextDecoder().decode(output(engine, handle, engine._web_engine_get_scene_snapshot)));
}
function apply(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,
`${payload.type}: ${engine.UTF8ToString(engine._web_engine_last_error_message())}`);
}
finally { engine._free(pointer); }
}
function reject(engine, handle, payload, code) {
const bytes = new TextEncoder().encode(JSON.stringify(payload));
const pointer = engine._malloc(bytes.byteLength);
try {
engine.HEAPU8.set(bytes, pointer);
assert.notEqual(engine._web_engine_apply_command(handle, pointer, bytes.byteLength), 0,
`${payload.type} unexpectedly succeeded`);
assert.match(engine.UTF8ToString(engine._web_engine_last_error_message()), new RegExp(code));
}
finally { engine._free(pointer); }
}
function close(actual, expected, label) {
assert.ok(Math.abs(actual - expected) <= 1e-6, `${label}: ${actual} != ${expected}`);
}
function assertUniformColor(mesh, expected) {
assert.equal(mesh.colors.length, mesh.cornerCount * 4);
for (let offset = 0; offset < mesh.colors.length; offset += 4) {
for (let channel = 0; channel < 4; channel++) {
close(mesh.colors[offset + channel], expected[channel], `color[${offset / 4}][${channel}]`);
}
}
}
function vertexWeight(mesh, vertex, groupName) {
const group = mesh.skinWeights?.boneNames.indexOf(groupName) ?? -1;
if (group < 0) return 0;
for (let slot = 0; slot < 4; slot++) {
const offset = vertex * 4 + slot;
if (mesh.skinWeights.indices[offset] === group) return mesh.skinWeights.weights[offset];
}
return 0;
}
const engine = await factory({ wasmBinary: wasmBinary.slice() });
const colorHandle = engine._web_engine_create();
open(engine, colorHandle, colorFixture);
const originalColorMesh = snapshot(engine, colorHandle).meshes.find((mesh) => mesh.id === "mesh:AttributeMesh");
assert.equal(originalColorMesh.cornerCount, 7);
const paintColor = [0.125, 0.375, 0.625, 0.875];
apply(engine, colorHandle, {
type: "setVertexColors",
meshId: originalColorMesh.id,
attributeName: "WebPaintColor",
domain: "CORNER",
indices: Array.from({ length: originalColorMesh.cornerCount }, (_, index) => index),
colors: Array.from({ length: originalColorMesh.cornerCount }, () => paintColor).flat(),
});
assertUniformColor(snapshot(engine, colorHandle).meshes.find((mesh) => mesh.id === originalColorMesh.id), paintColor);
assert.equal(engine._web_engine_undo(colorHandle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
assert.notDeepEqual(snapshot(engine, colorHandle).meshes.find((mesh) => mesh.id === originalColorMesh.id).colors,
Array.from({ length: originalColorMesh.cornerCount }, () => paintColor).flat());
assert.equal(engine._web_engine_redo(colorHandle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
assertUniformColor(snapshot(engine, colorHandle).meshes.find((mesh) => mesh.id === originalColorMesh.id), paintColor);
const savedColors = output(engine, colorHandle, engine._web_engine_save_blend, true);
engine._web_engine_destroy(colorHandle);
const reopenedColors = engine._web_engine_create();
open(engine, reopenedColors, savedColors);
assertUniformColor(snapshot(engine, reopenedColors).meshes.find((mesh) => mesh.id === originalColorMesh.id), paintColor);
engine._web_engine_destroy(reopenedColors);
const weightHandle = engine._web_engine_create();
open(engine, weightHandle, weightFixture);
const meshId = "mesh:RiggedShapeMesh";
const objectId = "object:RiggedShapeObject";
apply(engine, weightHandle, {
type: "setVertexWeights",
objectId,
vertexGroup: "WebPaintGroup",
indices: [0, 1],
values: [0.75, 0.25],
normalize: false,
});
let weightedMesh = snapshot(engine, weightHandle).meshes.find((mesh) => mesh.id === meshId);
close(vertexWeight(weightedMesh, 0, "WebPaintGroup"), 0.75, "vertex 0 paint weight");
close(vertexWeight(weightedMesh, 1, "WebPaintGroup"), 0.25, "vertex 1 paint weight");
apply(engine, weightHandle, {
type: "setVertexWeights",
objectId,
vertexGroup: "WebPaintGroup",
indices: [2],
values: [0.5],
normalize: true,
});
weightedMesh = snapshot(engine, weightHandle).meshes.find((mesh) => mesh.id === meshId);
const normalizedSum = weightedMesh.skinWeights.weights.slice(8, 12).reduce((sum, weight) => sum + weight, 0);
close(normalizedSum, 1, "normalized vertex 2 weight sum");
assert.equal(engine._web_engine_undo(weightHandle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
close(vertexWeight(snapshot(engine, weightHandle).meshes.find((mesh) => mesh.id === meshId), 2, "WebPaintGroup"), 0,
"undone vertex 2 paint weight");
assert.equal(engine._web_engine_redo(weightHandle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
close(vertexWeight(snapshot(engine, weightHandle).meshes.find((mesh) => mesh.id === meshId), 2, "WebPaintGroup"), 0.5 / 1.75,
"redone normalized vertex 2 paint weight");
reject(engine, weightHandle, {
type: "setVertexWeights",
objectId,
vertexGroup: "WebPaintGroup",
indices: [3],
values: [0.5],
mirror: true,
}, "CAPABILITY_MISSING");
const savedWeights = output(engine, weightHandle, engine._web_engine_save_blend, true);
engine._web_engine_destroy(weightHandle);
const reopenedWeights = engine._web_engine_create();
open(engine, reopenedWeights, savedWeights);
weightedMesh = snapshot(engine, reopenedWeights).meshes.find((mesh) => mesh.id === meshId);
close(vertexWeight(weightedMesh, 0, "WebPaintGroup"), 0.75, "reopened vertex 0 paint weight");
close(vertexWeight(weightedMesh, 2, "WebPaintGroup"), 0.5 / 1.75, "reopened normalized vertex 2 paint weight");
engine._web_engine_destroy(reopenedWeights);
process.stdout.write("paint-roundtrip-ok vertex-color=passed vertex-weight-normalize=passed mirror-gate=passed undo-redo=passed save-reopen=passed\n");