import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import createEngine from '../build_web_blender6/bin/web_engine.js'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const stages = []; const begin = performance.now(); const engine = await createEngine({ wasmBinary: fs.readFileSync(path.join(root, 'build_web_blender6/bin/web_engine.wasm')) }); stages.push({ stage: 'instantiate', seconds: (performance.now() - begin) / 1000 }); const handle = engine._web_engine_create(); const pointers = engine._malloc(8); const call = (stage, data) => { const started = performance.now(); const pointer = data ? engine._malloc(data.length) : 0; try { if (data) engine.HEAPU8.set(data, pointer); const code = stage === 'open' ? engine._web_engine_open_blend(handle, pointer, data.length) : stage === 'snapshot' ? engine._web_engine_get_scene_snapshot(handle, pointers, pointers + 4) : engine._web_engine_call_json(handle, pointer, data.length, pointers, pointers + 4); if (code !== 0) throw new Error(engine.UTF8ToString(engine._web_engine_last_error_message(handle))); if (stage === 'open') return null; const output = engine.HEAPU32[pointers / 4]; const length = engine.HEAPU32[pointers / 4 + 1]; return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(output, output + length))); } finally { if (pointer) engine._free(pointer); stages.push({ stage, seconds: (performance.now() - started) / 1000 }); } }; const edit = (command, expected = true) => { const result = call(command.type, Buffer.from(JSON.stringify({ cmd: 'applyCommand', params: { command } }))); assert.equal(result.ok, expected, result.error ?? command.type); }; try { const hierarchy = process.argv.includes('--hierarchy'); call('open', fs.readFileSync(path.join(root, 'tests/files/web', hierarchy ? 'object_editor_visibility_scene.blend' : 'object_editor_scene.blend'))); const initial = call('snapshot'); const cube = initial.nodes.find(node => node.name === 'BasicCube'); const collection = initial.collections.find(collection => collection.name === 'Collection'); assert(cube && collection); edit({ type: 'clearObjectTransform', objectId: cube.id, channels: ['LOCATION', 'ROTATION', 'SCALE'] }); const cleared = call('snapshot').nodes.find(node => node.id === cube.id); if (process.argv.includes('--inspect')) console.log(JSON.stringify({ transform: cleared.transform })); assert.deepEqual(cleared.transform.translation, [0, 0, 0]); assert.deepEqual(cleared.transform.rotationEuler, [0, 0, 0]); assert.deepEqual(cleared.transform.scale, [1, 1, 1]); for (const objectType of ['CAMERA', 'LIGHT', 'EMPTY', 'ARMATURE']) { const name = `Object Editor ${objectType}`; edit({ type: 'createObjectData', objectType, name }); assert.equal(call('snapshot').nodes.find(node => node.name === name)?.type, objectType); } edit({ type: 'setObjectSelection', objectId: cube.id, selected: true, active: true }); assert.equal(call('snapshot').activeObjectId, cube.id); edit({ type: 'setCollectionVisibility', collectionId: collection.id, viewportVisible: false, renderVisible: false }); const hidden = call('snapshot'); assert.equal(hidden.nodes.find(node => node.id === cube.id)?.visible, false, 'hidden collection must hide its objects in the Web snapshot'); if (hierarchy) { assert.equal(hidden.nodes.find(node => node.name === 'BasicCamera')?.visible, false); assert.equal(hidden.nodes.find(node => node.name === 'BasicArea')?.visible, true); assert.equal(hidden.nodes.find(node => node.name === 'ObjectEditor Hidden')?.visible, false); } edit({ type: 'setCollectionVisibility', collectionId: collection.id, viewportVisible: true, renderVisible: true }); const shown = call('snapshot'); assert.equal(shown.nodes.find(node => node.id === cube.id)?.visible, true); if (hierarchy) { assert.equal(shown.nodes.find(node => node.name === 'BasicCamera')?.visible, true); assert.equal(shown.nodes.find(node => node.name === 'BasicArea')?.visible, true); assert.equal(shown.nodes.find(node => node.name === 'ObjectEditor Hidden')?.visible, false); } for (const command of [ { type: 'clearObjectTransform', objectId: 'object:Missing', channels: ['LOCATION'] }, { type: 'createObjectData', objectType: 'UNSUPPORTED', name: 'Invalid' }, { type: 'setObjectSelection', objectId: 'object:Missing', selected: true, active: true }, { type: 'setCollectionVisibility', collectionId: 'collection:Missing', viewportVisible: false }, ]) edit(command, false); assert.equal(call('snapshot').nodes.length, initial.nodes.length + 4); console.log(JSON.stringify({ status: 'PASS', stages })); } catch (error) { console.log(JSON.stringify({ status: 'FAIL', error: error.message, frames: String(error.stack).split('\n').filter(line => /wasm-function|wasm:/.test(line)).slice(0, 10), stages })); process.exitCode = 1; } finally { engine._free(pointers); engine._web_engine_destroy(handle); }