import { expect, test, type Page } from '@playwright/test' interface CameraReport { position: { x: number; y: number; z: number } target: { x: number; y: number; z: number } fov: number distance: number } interface GuideDiagnostics { isInitialModelReady: () => boolean getReport: () => { camera: CameraReport; activeView: string; isCameraTweening: boolean } setCameraCalibration: (values: { yaw: number }) => void getFloors: () => Array<{ floorId: string; label: string }> switchFloor: (floorId: string) => Promise zoomCamera: (direction: 'in' | 'out', options?: { source?: 'button' | 'gesture' }) => void } const getCameraReport = (page: Page) => page.evaluate(() => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }).__GUIDE_3D_VISUAL_STABILITY__ if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable') return diagnostics.getReport().camera }) const getYawDegrees = (camera: CameraReport) => ( Math.atan2( camera.position.x - camera.target.x, camera.position.z - camera.target.z ) * 180 / Math.PI ) const getElevationDegrees = (camera: CameraReport) => { const x = camera.position.x - camera.target.x const y = camera.position.y - camera.target.y const z = camera.position.z - camera.target.z const distance = Math.hypot(x, y, z) return Math.asin(y / distance) * 180 / Math.PI } test('development camera calibration panel applies and resets exterior camera values', async ({ page }) => { await page.goto('/?camera-calibration=1') await page.waitForURL(/tab=guide/, { timeout: 30_000 }) await expect(page.locator('.camera-calibration-panel')).toBeVisible({ timeout: 30_000 }) await page.locator('[data-camera-calibration-action="open"]').click() await expect.poll(async () => page.evaluate(() => ( (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }) .__GUIDE_3D_VISUAL_STABILITY__?.isInitialModelReady() || false )), { timeout: 180_000 }).toBe(true) const before = await getCameraReport(page) const yawSlider = page.locator('[data-camera-calibration="yaw"]') await expect(yawSlider).toBeVisible() await page.evaluate(() => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }).__GUIDE_3D_VISUAL_STABILITY__ if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable') diagnostics.setCameraCalibration({ yaw: 35 }) }) await expect.poll(async () => Math.abs(getYawDegrees(await getCameraReport(page)) - 35), { timeout: 10_000 }).toBeLessThan(0.01) await page.locator('[data-camera-calibration-action="reset"]').click() await expect.poll(async () => Math.abs(getYawDegrees(await getCameraReport(page)) - 54), { timeout: 10_000 }).toBeLessThan(0.01) const after = await getCameraReport(page) expect(after.fov).toBeCloseTo(before.fov, 6) expect(after.distance).toBeGreaterThan(0) const floorId = await page.evaluate(() => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }).__GUIDE_3D_VISUAL_STABILITY__ const firstFloor = diagnostics?.getFloors().find((floor) => floor.label === '1F') || diagnostics?.getFloors()[0] if (!diagnostics || !firstFloor) throw new Error('Floor diagnostics unavailable') void diagnostics.switchFloor(firstFloor.floorId) return firstFloor.floorId }) await expect.poll(async () => page.evaluate(() => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }).__GUIDE_3D_VISUAL_STABILITY__ return diagnostics?.getReport() || null }), { timeout: 90_000 }).toMatchObject({ activeView: 'floor', isCameraTweening: false }) const indoor = await getCameraReport(page) expect(getYawDegrees(indoor)).toBeCloseTo(54, 6) expect(getElevationDegrees(indoor)).toBeCloseTo(42, 6) expect(indoor.fov).toBeCloseTo(42, 6) // Interior uses the exterior's visual target instead of each floor model's // bounding-box center, so the building cannot drift on the handoff. expect(indoor.target.x).toBeCloseTo(after.target.x, 6) expect(indoor.target.y).toBeCloseTo(after.target.y, 6) expect(indoor.target.z).toBeCloseTo(after.target.z, 6) expect(floorId).toBeTruthy() const floorTargets = await page.evaluate(() => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }).__GUIDE_3D_VISUAL_STABILITY__ if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable') return diagnostics.getFloors().filter((floor) => ['1F', 'MF', '3F', '4F', '5F'].includes(floor.label)) }) expect(floorTargets.map((floor) => floor.label).sort()).toEqual(['1F', '3F', '4F', '5F', 'MF']) for (const targetFloor of floorTargets) { await page.evaluate(async (targetFloorId) => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }).__GUIDE_3D_VISUAL_STABILITY__ if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable') await diagnostics.switchFloor(targetFloorId) }, targetFloor.floorId) await expect.poll(async () => page.evaluate(() => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }).__GUIDE_3D_VISUAL_STABILITY__ return diagnostics?.getReport().isCameraTweening || false }), { timeout: 90_000 }).toBe(false) const currentFloorCamera = await getCameraReport(page) expect(currentFloorCamera.distance).toBeCloseTo(indoor.distance, 6) expect(getYawDegrees(currentFloorCamera)).toBeCloseTo(54, 6) expect(getElevationDegrees(currentFloorCamera)).toBeCloseTo(42, 6) } let expectedZoomDistance = indoor.distance for (let count = 0; count < 5; count += 1) { expectedZoomDistance = Math.max(indoor.distance * 0.2, expectedZoomDistance * 0.72) await page.evaluate(() => { const diagnostics = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: GuideDiagnostics }).__GUIDE_3D_VISUAL_STABILITY__ if (!diagnostics) throw new Error('ThreeMap diagnostics unavailable') diagnostics.zoomCamera('in', { source: 'button' }) }) await expect.poll(async () => (await getCameraReport(page)).distance, { timeout: 10_000 }) .toBeCloseTo(expectedZoomDistance, 6) } })