152 lines
6.3 KiB
TypeScript
152 lines
6.3 KiB
TypeScript
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<void>
|
|
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)
|
|
}
|
|
|
|
})
|