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 fixture = fs.readFileSync(new URL("tests/files/web/basic_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 command(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); 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-5, `${label}: ${actual} != ${expected}`); } function assertLighting(scene) { const camera = scene.cameras.find((item) => item.id === "camera:Camera.001"); const light = scene.lights.find((item) => item.id === "light:Area"); const world = scene.worlds.find((item) => item.id === "world:World"); const definition = scene.scenes.find((item) => item.id === "scene:Scene"); assert.equal(camera.projection, "PERSPECTIVE"); assert.equal(camera.depthOfField.enabled, false); assert.equal(camera.depthOfField.focusDistance, 10); assert.equal(light.energy, 800); assert.equal(light.exposure, 0); assert.equal(light.castsShadow, true); assert.equal(light.temperature, 6500); assert.equal(world.mist.enabled, false); assert.equal(world.mist.type, "QUADRATIC"); assert.equal(definition.renderEngine, "BLENDER_EEVEE"); assert.equal(definition.colorManagement.displayDevice, "sRGB"); assert.equal(definition.colorManagement.viewTransform, "AgX"); assert.equal(definition.colorManagement.look, "None"); assert.equal(definition.colorManagement.exposure, 0); assert.equal(definition.colorManagement.whiteBalanceStatus, "AVAILABLE"); close(definition.colorManagement.temperature, 6500, "scene temperature"); close(definition.colorManagement.tint, 10, "scene tint"); return { camera, light, world, definition }; } function assertEdited(scene) { const camera = scene.cameras.find((item) => item.id === "camera:Camera.001"); const light = scene.lights.find((item) => item.id === "light:Area"); const world = scene.worlds.find((item) => item.id === "world:World"); const definition = scene.scenes.find((item) => item.id === "scene:Scene"); assert.equal(camera.projection, "ORTHOGRAPHIC"); close(camera.lensMm, 35, "camera lens"); close(camera.sensorWidthMm, 32, "camera sensor width"); close(camera.sensorHeightMm, 18, "camera sensor height"); assert.equal(camera.sensorFit, 1); close(camera.shift[0], 0.1, "camera shift x"); close(camera.shift[1], -0.2, "camera shift y"); close(camera.near, 0.2, "camera near"); close(camera.far, 500, "camera far"); close(camera.orthoScale, 8, "camera ortho scale"); assert.equal(camera.depthOfField.enabled, true); close(camera.depthOfField.focusDistance, 4.5, "camera focus distance"); close(camera.depthOfField.apertureFStop, 1.8, "camera aperture f-stop"); assert.equal(camera.depthOfField.apertureBlades, 7); close(camera.depthOfField.apertureRotation, 0.25, "camera aperture rotation"); close(camera.depthOfField.apertureRatio, 1.2, "camera aperture ratio"); close(light.energy, 400, "light energy"); close(light.exposure, 1, "light exposure"); assert.equal(light.castsShadow, false); assert.equal(light.useTemperature, true); close(light.temperature, 5000, "light temperature"); assert.deepEqual(light.color, [0.25, 0.5, 0.75]); close(light.radius, 0.3, "light radius"); close(light.spotAngle, 1.1, "light spot angle"); close(light.spotBlend, 0.25, "light spot blend"); close(light.areaSize, 3, "light area size"); close(light.areaSizeY, 2, "light area size y"); close(light.areaSpread, 2.4, "light area spread"); close(light.sunAngle, 0.1, "light sun angle"); assert.equal(world.mist.enabled, true); assert.equal(world.mist.type, "LINEAR"); close(world.mist.start, 2, "mist start"); close(world.mist.depth, 50, "mist depth"); close(world.mist.intensity, 0.2, "mist intensity"); close(definition.colorManagement.exposure, 0, "preserved scene exposure"); close(definition.colorManagement.gamma, 1, "preserved scene gamma"); assert.equal(definition.colorManagement.whiteBalanceStatus, "BLOCKED"); assert.equal(definition.colorManagement.temperature, undefined); assert.equal(definition.colorManagement.tint, undefined); return { camera, light, world, definition }; } const engine = await factory({ wasmBinary: wasmBinary.slice() }); const handle = engine._web_engine_create(); open(engine, handle, fixture); const before = assertLighting(snapshot(engine, handle)); command(engine, handle, { type: "setObjectVisibility", objectId: "object:BasicCube", visible: true }); assert.equal(snapshot(engine, handle).scenes.find((item) => item.id === before.definition.id).colorManagement.whiteBalanceStatus, "AVAILABLE"); reject(engine, handle, { type: "setCameraProperties", dataId: before.camera.id, properties: { near: 10, far: 1 } }, "RENDER_PROPERTY_INVALID"); assert.equal(snapshot(engine, handle).cameras.find((item) => item.id === before.camera.id).near, before.camera.near); command(engine, handle, { type: "setCameraProperties", dataId: before.camera.id, properties: { projection: "ORTHOGRAPHIC", lensMm: 35, sensorWidthMm: 32, sensorHeightMm: 18, sensorFit: 1, shift: [0.1, -0.2], near: 0.2, far: 500, orthoScale: 8, depthOfField: { enabled: true, focusDistance: 4.5, apertureFStop: 1.8, apertureBlades: 7, apertureRotation: 0.25, apertureRatio: 1.2 }, } }); let cameraEdited = snapshot(engine, handle); assert.equal(cameraEdited.scenes.find((item) => item.id === before.definition.id).colorManagement.whiteBalanceStatus, "BLOCKED"); assert.equal(cameraEdited.cameras.find((item) => item.id === before.camera.id).projection, "ORTHOGRAPHIC"); assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); assert.equal(snapshot(engine, handle).cameras.find((item) => item.id === before.camera.id).projection, "PERSPECTIVE"); assert.equal(engine._web_engine_redo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); cameraEdited = snapshot(engine, handle); assert.equal(cameraEdited.cameras.find((item) => item.id === before.camera.id).projection, "ORTHOGRAPHIC"); command(engine, handle, { type: "setLightProperties", dataId: before.light.id, properties: { color: [0.25, 0.5, 0.75], energy: 400, exposure: 1, castsShadow: false, temperature: 5000, useTemperature: true, radius: 0.3, spotAngle: 1.1, spotBlend: 0.25, areaSize: 3, areaSizeY: 2, areaSpread: 2.4, sunAngle: 0.1, } }); assert.equal(snapshot(engine, handle).scenes.find((item) => item.id === before.definition.id).colorManagement.whiteBalanceStatus, "BLOCKED"); command(engine, handle, { type: "setWorldProperties", dataId: before.world.id, properties: { color: [0.1, 0.2, 0.3], exposure: 0.5, mist: { enabled: true, type: "LINEAR", start: 2, depth: 50, intensity: 0.2, height: 3 }, } }); assert.equal(snapshot(engine, handle).scenes.find((item) => item.id === before.definition.id).colorManagement.whiteBalanceStatus, "BLOCKED"); assertEdited(snapshot(engine, handle)); assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); assert.equal(snapshot(engine, handle).worlds.find((item) => item.id === before.world.id).mist.enabled, false); assert.equal(engine._web_engine_redo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); assertEdited(snapshot(engine, handle)); const saved = output(engine, handle, engine._web_engine_save_blend, true); engine._web_engine_destroy(handle); const reopened = engine._web_engine_create(); open(engine, reopened, saved); assertEdited(snapshot(engine, reopened)); engine._web_engine_destroy(reopened); process.stdout.write("lighting-roundtrip-ok camera-writer-dof=passed light-shadow-exposure=passed world-mist=passed color-management-reader=passed white-balance-gate=passed undo-redo=passed save-reopen=passed\n");