167 lines
7.3 KiB
JavaScript
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");
|