This commit is contained in:
151
tests/e2e/camera-calibration.spec.ts
Normal file
151
tests/e2e/camera-calibration.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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)
|
||||
}
|
||||
|
||||
})
|
||||
Reference in New Issue
Block a user