修复跨楼层点位聚焦视觉跳动
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-14 01:16:31 +08:00
parent 231ecb465e
commit abaeda189e
3 changed files with 296 additions and 240 deletions

View File

@@ -30,6 +30,14 @@ interface VisualReport {
activeView: string
floorId: string
isCameraTweening: boolean
poiFocusCameraAnimationCount: number
cameraSnapshotRestoreCount: number
focus: {
poiId: string
floorId: string
positionGltf: [number, number, number]
markerPosition: { x: number; y: number; z: number } | null
} | null
}
interface FloorReport {
@@ -38,11 +46,27 @@ interface FloorReport {
modelMatchKeys: string[]
}
interface PoiFocusCandidate {
poiId: string
floorId: string
name: string
positionGltf: [number, number, number]
}
interface VisualStabilityApi {
getReport: () => VisualReport
getFloors: () => FloorReport[]
switchFloor: (floorId: string) => Promise<void>
showOverview: () => Promise<void>
getFloorPois: (floorId: string) => Promise<PoiFocusCandidate[]>
focusTargetPoi: (request: {
requestId: number | string
poiId: string
floorId: string
name?: string
positionGltf: [number, number, number]
}) => Promise<unknown>
clearTargetFocus: () => void
}
const getApiState = async (page: Page) => {
@@ -219,6 +243,43 @@ const resolveRequiredFloorId = (floors: FloorReport[], requested: string) => {
return floor!.floorId
}
const getFloorPois = async (page: Page, floorId: string) => {
const pois = await page.evaluate(async (id) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
.__GUIDE_3D_VISUAL_STABILITY__
if (!api) throw new Error('ThreeMap visual stability diagnostics unavailable')
return api.getFloorPois(id)
}, floorId)
expect(pois.length, `floor ${floorId} must contain a positioned POI`).toBeGreaterThan(0)
return pois
}
const getVectorDelta = (left: { x: number; y: number; z: number }, right: [number, number, number]) => (
Math.hypot(left.x - right[0], left.y - right[1], left.z - right[2])
)
const waitForPoiFocusToSettle = async (page: Page, testInfo: TestInfo, name: string) => {
let sawTween = false
let stableFrames = 0
for (let frame = 0; frame < 180; frame += 1) {
const report = await getReport(page)
expect(report.modelRoot, `blank POI focus frame ${frame}`).not.toBeNull()
expect(report.modelRoot!.maxError).toBeLessThan(1e-12)
sawTween ||= report.isCameraTweening
stableFrames = report.isCameraTweening ? 0 : stableFrames + 1
if (frame === 0) {
await screenshot(page, testInfo, `${name}-during`)
}
if (sawTween && stableFrames >= 3) return report
await page.waitForTimeout(40)
}
throw new Error(`${name} did not settle after one POI focus animation`)
}
test('ordinary exterior and floor switches preserve the GLB_METER camera projection', async ({ page }, testInfo) => {
await page.goto('/')
await Promise.all([
@@ -235,14 +296,14 @@ test('ordinary exterior and floor switches preserve the GLB_METER camera project
const lMinus2 = resolveRequiredFloorId(state!.floors, 'L-2')
const l1 = resolveRequiredFloorId(state!.floors, 'L1')
const l1Point5 = resolveRequiredFloorId(state!.floors, 'L1.5')
const l2 = resolveRequiredFloorId(state!.floors, 'L2')
const l3 = resolveRequiredFloorId(state!.floors, 'L3')
const lMinus1 = resolveRequiredFloorId(state!.floors, 'L-1')
const l4 = resolveRequiredFloorId(state!.floors, 'L4')
const l5 = resolveRequiredFloorId(state!.floors, 'L5')
await switchFloor(page, testInfo, l1, 'exterior-l1')
await switchFloor(page, testInfo, l2, 'l1-l2')
await switchFloor(page, testInfo, l3, 'l2-l3')
await switchFloor(page, testInfo, l1, 'l3-l1')
await switchFloor(page, testInfo, lMinus1, 'l1-lminus1')
await switchFloor(page, testInfo, l4, 'lminus1-l4')
await switchFloor(page, testInfo, l1, 'l4-l1')
await showOverview(page, testInfo, 'l1-exterior')
await switchFloor(page, testInfo, lMinus2, 'exterior-lminus2')
@@ -254,14 +315,87 @@ test('ordinary exterior and floor switches preserve the GLB_METER camera project
await showOverview(page, testInfo, 'l1point5-exterior')
await switchFloor(page, testInfo, l1, 'exterior-l1-auto-exit')
await switchFloor(page, testInfo, l2, 'l1-l2-auto-exit')
await exitOverviewWithZoomInput(page, testInfo, 'l2-exterior-button-exit', 'button')
await switchFloor(page, testInfo, lMinus1, 'l1-lminus1-auto-exit')
await exitOverviewWithZoomInput(page, testInfo, 'lminus1-exterior-button-exit', 'button')
await switchFloor(page, testInfo, l1, 'exterior-l1-wheel-exit')
await switchFloor(page, testInfo, l2, 'l1-l2-wheel-exit')
await exitOverviewWithZoomInput(page, testInfo, 'l2-exterior-wheel-exit', 'wheel')
await switchFloor(page, testInfo, lMinus1, 'l1-lminus1-wheel-exit')
await exitOverviewWithZoomInput(page, testInfo, 'lminus1-exterior-wheel-exit', 'wheel')
await switchFloor(page, testInfo, l1, 'exterior-l1-touch-exit')
await switchFloor(page, testInfo, l2, 'l1-l2-touch-exit')
await exitOverviewWithZoomInput(page, testInfo, 'l2-exterior-touch-exit', 'touch')
await switchFloor(page, testInfo, lMinus1, 'l1-lminus1-touch-exit')
await exitOverviewWithZoomInput(page, testInfo, 'lminus1-exterior-touch-exit', 'touch')
})
test('queued cross-floor search POI focus keeps raw GLB_METER coordinates and does not rebound', async ({ page }, testInfo) => {
await page.goto('/')
await Promise.all([
page.waitForURL(/tab=guide/, { timeout: 30_000 }),
page.getByText('馆内', { exact: true }).click()
])
await expect.poll(async () => (await getApiState(page)) !== null, { timeout: 180_000 }).toBe(true)
const state = await getApiState(page)
expect(state).not.toBeNull()
const l1 = resolveRequiredFloorId(state!.floors, 'L1')
const lMinus1 = resolveRequiredFloorId(state!.floors, 'L-1')
const [targetPoi] = await getFloorPois(page, lMinus1)
await switchFloor(page, testInfo, l1, 'search-focus-exterior-l1')
const beforeFocus = await getReport(page)
await screenshot(page, testInfo, 'search-focus-before')
const focusTransition = page.evaluate(async (request) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
.__GUIDE_3D_VISUAL_STABILITY__
if (!api) throw new Error('ThreeMap visual stability diagnostics unavailable')
await api.focusTargetPoi(request)
}, {
requestId: 'e2e-cross-floor-search-focus',
poiId: targetPoi.poiId,
floorId: targetPoi.floorId,
name: targetPoi.name,
positionGltf: targetPoi.positionGltf
})
let focusRequestCompleted = false
void focusTransition.then(() => { focusRequestCompleted = true })
for (let frame = 0; !focusRequestCompleted && frame < 180; frame += 1) {
const report = await getReport(page)
expect(report.modelRoot, `blank cross-floor POI load frame ${frame}`).not.toBeNull()
expect(report.modelRoot!.maxError).toBeLessThan(1e-12)
await page.waitForTimeout(40)
}
expect(focusRequestCompleted, 'cross-floor POI focus exceeded the sampling window').toBe(true)
await focusTransition
const afterFocus = await waitForPoiFocusToSettle(page, testInfo, 'search-focus')
await screenshot(page, testInfo, 'search-focus-after')
expect(afterFocus.activeView).toBe('floor')
expect(afterFocus.floorId).toBe(lMinus1)
expect(afterFocus.focus).not.toBeNull()
expect(afterFocus.focus!.poiId).toBe(targetPoi.poiId)
expect(afterFocus.focus!.positionGltf).toEqual(targetPoi.positionGltf)
expect(getVectorDelta(afterFocus.camera.target, targetPoi.positionGltf)).toBeLessThan(1e-6)
expect(afterFocus.focus!.markerPosition).not.toBeNull()
expect(getVectorDelta(afterFocus.focus!.markerPosition!, targetPoi.positionGltf)).toBeLessThan(1e-6)
expect(afterFocus.cameraSnapshotRestoreCount - beforeFocus.cameraSnapshotRestoreCount).toBe(1)
expect(afterFocus.poiFocusCameraAnimationCount - beforeFocus.poiFocusCameraAnimationCount).toBe(1)
await page.waitForTimeout(240)
const afterIdle = await getReport(page)
expect(afterIdle.isCameraTweening).toBe(false)
expect(afterIdle.poiFocusCameraAnimationCount).toBe(afterFocus.poiFocusCameraAnimationCount)
expect(getMaximumCameraDelta(afterFocus.camera, afterIdle.camera)).toBeLessThan(1e-6)
await page.evaluate(() => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
.__GUIDE_3D_VISUAL_STABILITY__
if (!api) throw new Error('ThreeMap visual stability diagnostics unavailable')
api.clearTargetFocus()
})
await switchFloor(page, testInfo, l1, 'search-focus-clear-lminus1-l1')
await showOverview(page, testInfo, 'search-focus-clear-l1-exterior')
})

View File

@@ -156,7 +156,7 @@ describe('点位详情实际渲染与返回闭环', () => {
const sheet = wrapper.get('[data-testid="facility-detail-sheet"]')
const mapShell = wrapper.getComponent(GuideMapShellStub)
expect(sheet.text()).toContain('二层卫生间')
expect(sheet.text()).toContain('类别卫生间')
expect(sheet.text()).toContain('类别洗手间')
expect(sheet.text()).toContain('楼层2F')
expect(sheet.text()).not.toContain('可无障碍到达')
expect(sheet.text()).not.toContain('展示点位')