import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; const root = path.resolve(import.meta.dirname, "../../.."); const blend = fs.readFileSync(path.join(root, "tests/files/web/basic_scene.blend")); const expected = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-02/lighting-field-roundtrip.json"), "utf8")); function expectClose(actual: unknown, reference: unknown): void { if (typeof reference === "number") { expect(actual).toBeCloseTo(reference, 5); return; } if (Array.isArray(reference)) { expect(Array.isArray(actual)).toBe(true); expect(actual).toHaveLength(reference.length); reference.forEach((value, index) => expectClose((actual as unknown[])[index], value)); return; } if (reference && typeof reference === "object") { expect(actual && typeof actual === "object").toBe(true); for (const [field, value] of Object.entries(reference)) { expectClose((actual as Record)[field], value); } return; } expect(actual).toEqual(reference); } test("M11-02 edits, undoes, redoes, reopens and maps supported lighting fields", async ({ page }) => { test.setTimeout(60_000); await page.goto("/"); const result = await page.evaluate(async (input) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const { configurePBRCamera, configurePBRLight, createPBRLight } = await import("/src/three-adapter/pbr.ts"); const { Object3D, PerspectiveCamera } = await import("/src/vendor/three/three.module.js"); const client = new WebEngineClient({ timeoutMs: 30_000 }); const reopened = new WebEngineClient({ timeoutMs: 30_000 }); try { const opened = await client.openBlend(Uint8Array.from(input).buffer); const camera = opened.snapshot.cameras.find((item) => item.id === "camera:Camera.001")!; const light = opened.snapshot.lights.find((item) => item.id === "light:Area")!; const world = opened.snapshot.worlds.find((item) => item.id === "world:World")!; const revisions = [opened.snapshot.revision]; const cameraEdit = await client.applyCommand({ type: "setCameraProperties", dataId: camera.id, properties: { lensMm: 35, sensorWidthMm: 32, sensorHeightMm: 18, sensorFit: 2, 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 }, } }); revisions.push(cameraEdit.snapshot.revision); const cameraUndo = await client.applyCommand({ type: "undo" }); revisions.push(cameraUndo.snapshot.revision); const cameraRedo = await client.applyCommand({ type: "redo" }); revisions.push(cameraRedo.snapshot.revision); const lightEdit = await client.applyCommand({ type: "setLightProperties", dataId: light.id, properties: { color: [0.25, 0.5, 0.75], energy: 400, exposure: 1, temperature: 5000, useTemperature: true, castsShadow: false, radius: 0.3, spotAngle: 1.1, spotBlend: 0.25, areaSize: 3, areaSizeY: 2, areaSpread: 2.4, sunAngle: 0.1, } }); revisions.push(lightEdit.snapshot.revision); const lightUndo = await client.applyCommand({ type: "undo" }); revisions.push(lightUndo.snapshot.revision); const lightRedo = await client.applyCommand({ type: "redo" }); revisions.push(lightRedo.snapshot.revision); const worldEdit = await client.applyCommand({ type: "setWorldProperties", dataId: 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 }, } }); revisions.push(worldEdit.snapshot.revision); const worldUndo = await client.applyCommand({ type: "undo" }); revisions.push(worldUndo.snapshot.revision); const worldRedo = await client.applyCommand({ type: "redo" }); revisions.push(worldRedo.snapshot.revision); const saved = await client.saveBlend(); const reopenedResult = await reopened.openBlend(saved); const reopenedCamera = reopenedResult.snapshot.cameras.find((item) => item.id === camera.id)!; const reopenedLight = reopenedResult.snapshot.lights.find((item) => item.id === light.id)!; const reopenedWorld = reopenedResult.snapshot.worlds.find((item) => item.id === world.id)!; const mappedCamera = new PerspectiveCamera(); configurePBRCamera(mappedCamera, reopenedCamera); const horizontalCamera = new PerspectiveCamera(); configurePBRCamera(horizontalCamera, { ...reopenedCamera, sensorFit: 1 }); const spot = createPBRLight({ ...reopenedLight, lightType: 2 }); const area = createPBRLight({ ...reopenedLight, lightType: 4 }); const lightNode = reopenedResult.snapshot.nodes.find((item) => item.dataId === light.id)!; configurePBRLight(spot, lightNode, new Object3D()); return { revisions, cameraUndoLens: cameraUndo.snapshot.cameras.find((item) => item.id === camera.id)!.lensMm, cameraRedoLens: cameraRedo.snapshot.cameras.find((item) => item.id === camera.id)!.lensMm, lightUndoEnergy: lightUndo.snapshot.lights.find((item) => item.id === light.id)!.energy, lightRedoEnergy: lightRedo.snapshot.lights.find((item) => item.id === light.id)!.energy, worldUndoColor: worldUndo.snapshot.worlds.find((item) => item.id === world.id)!.color, worldRedoColor: worldRedo.snapshot.worlds.find((item) => item.id === world.id)!.color, camera: reopenedCamera, light: reopenedLight, world: reopenedWorld, viewport: { camera: { fov: mappedCamera.fov, horizontalFov: horizontalCamera.fov, near: mappedCamera.near, far: mappedCamera.far, filmGauge: mappedCamera.filmGauge, filmOffset: mappedCamera.filmOffset }, spot: { color: spot.color.toArray(), intensity: spot.intensity, angle: "angle" in spot ? spot.angle : 0, penumbra: "penumbra" in spot ? spot.penumbra : 0, castShadow: spot.castShadow }, area: { intensity: area.intensity, width: "width" in area ? area.width : 0, height: "height" in area ? area.height : 0 }, worldColor: reopenedWorld.color, }, }; } finally { client.terminate(); reopened.terminate(); } }, Array.from(blend)); expect(result.revisions.every((revision, index) => index === 0 || revision === result.revisions[index - 1] + 1)).toBe(true); expect(result.cameraUndoLens).toBe(50); expect(result.cameraRedoLens).toBe(expected.camera.lensMm); expect(result.lightUndoEnergy).toBe(800); expect(result.lightRedoEnergy).toBe(expected.light.energy); expect(result.worldUndoColor).not.toEqual(expected.world.color); expectClose(result.worldRedoColor, expected.world.color); for (const [field, value] of Object.entries(expected.camera).filter(([field]) => !field.startsWith("viewport"))) { expectClose(result.camera[field as keyof typeof result.camera], value); } for (const [field, value] of Object.entries(expected.light).filter(([field]) => !field.startsWith("viewport"))) { expectClose(result.light[field as keyof typeof result.light], value); } expectClose(result.world, expected.world); expect(result.viewport.camera.fov).toBeCloseTo(expected.camera.viewportFov, 5); expect(result.viewport.camera.horizontalFov).toBeCloseTo(expected.camera.viewportHorizontalFov, 5); expectClose(result.viewport.camera.near, expected.camera.near); expectClose(result.viewport.camera.far, expected.camera.far); expectClose(result.viewport.camera.filmGauge, expected.camera.viewportFilmGauge); expect(result.viewport.camera.filmOffset).toBeCloseTo(expected.camera.viewportFilmOffset, 6); expectClose(result.viewport.spot.intensity, expected.light.viewportIntensity); expectClose(result.viewport.spot.color, expected.light.viewportColor); expectClose(result.viewport.spot.angle, expected.light.spotAngle); expectClose(result.viewport.spot.penumbra, expected.light.spotBlend); expect(result.viewport.spot.castShadow).toBe(false); expectClose(result.viewport.area, { intensity: expected.light.viewportIntensity, width: expected.light.areaSize, height: expected.light.areaSizeY }); expectClose(result.viewport.worldColor, expected.world.color); });