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 golden = JSON.parse(fs.readFileSync(new URL("tests/golden/N-015/nonmesh-desktop-geometry.json", root), "utf8")); const fixture = fs.readFileSync(new URL(`tests/files/web/${golden.fixture}`, root)); function readOutput(engine, handle, callback) { const dataOut = engine._malloc(4); const lengthOut = engine._malloc(4); try { assert.equal(callback(handle, dataOut, lengthOut), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); 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 close(actual, expected, tolerance, label) { assert.ok(Math.abs(actual - expected) <= tolerance, `${label}: expected ${expected}, got ${actual}`); } function metrics(geometry) { const positions = geometry.positions ?? []; const edgeIndices = geometry.edgeVertexIndices ?? []; const indices = geometry.indices ?? []; const vertexCount = positions.length / 3; const boundsMin = [0, 1, 2].map((axis) => vertexCount ? Math.min(...Array.from({ length: vertexCount }, (_, index) => positions[index * 3 + axis])) : 0); const boundsMax = [0, 1, 2].map((axis) => vertexCount ? Math.max(...Array.from({ length: vertexCount }, (_, index) => positions[index * 3 + axis])) : 0); const centroid = [0, 1, 2].map((axis) => vertexCount ? Array.from({ length: vertexCount }, (_, index) => positions[index * 3 + axis]).reduce((sum, value) => sum + value, 0) / vertexCount : 0); let surfaceArea = 0; for (let triangle = 0; triangle < indices.length; triangle += 3) { const a = indices[triangle] * 3; const b = indices[triangle + 1] * 3; const c = indices[triangle + 2] * 3; const ab = [positions[b] - positions[a], positions[b + 1] - positions[a + 1], positions[b + 2] - positions[a + 2]]; const ac = [positions[c] - positions[a], positions[c + 1] - positions[a + 1], positions[c + 2] - positions[a + 2]]; const cross = [ab[1] * ac[2] - ab[2] * ac[1], ab[2] * ac[0] - ab[0] * ac[2], ab[0] * ac[1] - ab[1] * ac[0]]; surfaceArea += Math.hypot(...cross) * 0.5; } return { vertexCount, edgeCount: edgeIndices.length / 2, triangleCount: indices.length / 3, boundsMin, boundsMax, centroid, surfaceArea, positionMoment: positions.reduce((sum, value, index) => sum + (index + 1) * value, 0), edgeIndexMoment: edgeIndices.reduce((sum, value, index) => sum + (index + 1) * (value + 1), 0), indexMoment: indices.reduce((sum, value, index) => sum + (index + 1) * (value + 1), 0), }; } assert.equal(golden.schemaVersion, 1); assert.equal(golden.blenderVersion, "5.2.0 LTS"); const engine = await factory({ wasmBinary }); const handle = engine._web_engine_create(); try { const input = engine._malloc(fixture.byteLength); engine.HEAPU8.set(fixture, input); try { assert.equal(engine._web_engine_open_blend(handle, input, fixture.byteLength), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); } finally { engine._free(input); } const evaluation = readOutput(engine, handle, engine._web_engine_evaluate_depsgraph); const byObject = new Map(evaluation.nonMeshGeometries.map((geometry) => [geometry.objectId.replace(/^object:/, ""), geometry])); for (const expected of golden.geometries) { const geometry = byObject.get(expected.object); assert.ok(geometry, `desktop golden object ${expected.object} is missing from the Web evaluation`); assert.equal(geometry.status, "EVALUATED"); assert.equal(geometry.sourceType, expected.sourceType); const actual = metrics(geometry); assert.equal(actual.vertexCount, expected.vertexCount, `${expected.object} vertex count`); assert.equal(actual.edgeCount, expected.edgeCount, `${expected.object} edge count`); assert.equal(actual.triangleCount, expected.triangleCount, `${expected.object} triangle count`); assert.equal(actual.edgeIndexMoment, expected.edgeIndexMoment, `${expected.object} edge index moment`); assert.equal(actual.indexMoment, expected.indexMoment, `${expected.object} index moment`); for (let axis = 0; axis < 3; axis++) { close(actual.boundsMin[axis], expected.boundsMin[axis], golden.tolerance.coordinate, `${expected.object} boundsMin[${axis}]`); close(actual.boundsMax[axis], expected.boundsMax[axis], golden.tolerance.coordinate, `${expected.object} boundsMax[${axis}]`); close(actual.centroid[axis], expected.centroid[axis], golden.tolerance.coordinate, `${expected.object} centroid[${axis}]`); } close(actual.surfaceArea, expected.surfaceArea, golden.tolerance.surfaceArea, `${expected.object} surface area`); close(actual.positionMoment, expected.positionMoment, golden.tolerance.positionMoment, `${expected.object} position moment`); } assert.equal(byObject.size, golden.geometries.length, "unexpected evaluated non-mesh object count"); } finally { engine._web_engine_destroy(handle); } console.log(`nonmesh-desktop-golden-ok fixture=${golden.fixture} geometries=${golden.geometries.length}`);