Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
92 lines
5.0 KiB
JavaScript
92 lines
5.0 KiB
JavaScript
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);
|
|
}
|