修复导览楼层默认进入与模型复位状态
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-17 11:54:08 +08:00
parent 19ecca84e8
commit 122013ccfb
7 changed files with 407 additions and 69 deletions

View File

@@ -52,11 +52,23 @@ interface PoiFocusCandidate {
poiId: string
floorId: string
name: string
kind?: string
primaryCategory: string
positionGltf: [number, number, number]
}
interface VisiblePoiScreenPosition {
poiId: string
floorId: string
name: string
kind?: string
primaryCategory: string
screen: { x: number; y: number }
}
interface VisualStabilityApi {
getReport: () => VisualReport
isInitialModelReady: () => boolean
getFloors: () => FloorReport[]
switchFloor: (floorId: string) => Promise<void>
showOverview: () => Promise<void>
@@ -64,10 +76,11 @@ interface VisualStabilityApi {
view: 'overview' | 'floor'
floorId?: string
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
}) => Promise<boolean>
resetToInitialState: () => Promise<boolean>
}) => Promise<'applied' | 'not-ready' | 'invalid-target' | 'failed' | 'stale'>
resetToInitialState: () => Promise<'applied' | 'not-ready' | 'invalid-target' | 'failed' | 'stale'>
getFloorBaseline: (floorId: string) => CameraReport | null
getFloorPois: (floorId: string) => Promise<PoiFocusCandidate[]>
getVisiblePoiScreenPositions: () => VisiblePoiScreenPosition[]
focusTargetPoi: (request: {
requestId: number | string
poiId: string
@@ -103,6 +116,20 @@ const getReport = async (page: Page) => {
return state!.report
}
const openGuide = async (page: Page) => {
await page.goto('/')
await page.waitForURL(/tab=guide/, { timeout: 30_000 })
await expect.poll(async () => (await getApiState(page)) !== null, { timeout: 180_000 }).toBe(true)
await expect.poll(async () => (await getApiState(page))?.floors.length ?? 0, { timeout: 180_000 })
.toBeGreaterThan(0)
await expect.poll(async () => page.evaluate(() => (
(window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
.__GUIDE_3D_VISUAL_STABILITY__?.isInitialModelReady() || false
)), { timeout: 180_000 }).toBe(true)
await expect.poll(async () => (await getApiState(page))?.report.modelRoot !== null, { timeout: 180_000 }).toBe(true)
await expect.poll(async () => (await getApiState(page))?.report.anchors.length ?? 0, { timeout: 180_000 }).toBe(3)
}
const getMaximumCameraDelta = (before: CameraReport, after: CameraReport) => Math.max(
Math.hypot(
before.position.x - after.position.x,
@@ -263,6 +290,13 @@ const getFloorPois = async (page: Page, floorId: string) => {
return pois
}
const getVisiblePoiScreenPositions = async (page: Page) => 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')
return api.getVisiblePoiScreenPositions()
})
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])
)
@@ -290,12 +324,7 @@ const waitForPoiFocusToSettle = async (page: Page, testInfo: TestInfo, name: str
}
test('ordinary exterior and floor switches preserve the GLB_METER camera projection', 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)
await openGuide(page)
await expect.poll(async () => (await getApiState(page))?.report.anchors.length ?? 0, {
timeout: 180_000
}).toBe(3)
@@ -337,12 +366,7 @@ test('ordinary exterior and floor switches preserve the GLB_METER camera project
})
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)
await openGuide(page)
const state = await getApiState(page)
expect(state).not.toBeNull()
@@ -410,12 +434,7 @@ test('queued cross-floor search POI focus keeps raw GLB_METER coordinates and do
})
test('reset invalidates an in-flight cross-floor focus before it can overwrite the initial state', async ({ page }) => {
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)
await openGuide(page)
await expect.poll(async () => (await getApiState(page))?.floors.length ?? 0, { timeout: 180_000 }).toBeGreaterThan(0)
const state = await getApiState(page)
expect(state).not.toBeNull()
@@ -440,11 +459,7 @@ test('reset invalidates an in-flight cross-floor focus before it can overwrite t
})
test('floor view baseline is deterministic across repeated resets', async ({ page }) => {
await page.goto('/')
await Promise.all([
page.waitForURL(/tab=guide/, { timeout: 30_000 }),
page.getByText('馆内', { exact: true }).click()
])
await openGuide(page)
await expect.poll(async () => (await getApiState(page))?.floors.length ?? 0, { timeout: 180_000 }).toBeGreaterThan(0)
const state = await getApiState(page)
expect(state).not.toBeNull()
@@ -478,3 +493,69 @@ test('floor view baseline is deterministic across repeated resets', async ({ pag
expect(getMaximumCameraDelta(baseline!, second.camera)).toBeLessThan(1e-6)
}
})
test.describe('mobile POI card close restores the floor baseline', () => {
test.use({ viewport: { width: 390, height: 844 } })
test('real canvas clicks on a facility and hall close back to their floor baselines', async ({ page }) => {
await openGuide(page)
const state = await getApiState(page)
expect(state).not.toBeNull()
const floorId = state!.report.floorId
await page.evaluate(async (targetFloorId) => {
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.resetToViewBaseline({ view: 'floor', floorId: targetFloorId, reason: 'floor-reset' })
}, floorId)
await expect.poll(async () => getVisiblePoiScreenPositions(page), { timeout: 30_000 })
.not.toHaveLength(0)
const visibleMarkers = await getVisiblePoiScreenPositions(page)
const facility = visibleMarkers.find((poi) => (
poi.kind !== 'hall'
&& !poi.primaryCategory.startsWith('exhibition_hall')
))
const hall = visibleMarkers.find((poi) => (
poi.kind === 'hall'
|| poi.primaryCategory.startsWith('exhibition_hall')
))
expect(facility, 'expected a visible facility POI from the active guide data').toBeDefined()
expect(hall, 'expected a visible hall POI from the active guide data').toBeDefined()
for (const [kind, marker] of [['facility', facility!], ['hall', hall!]] as const) {
const baseline = await page.evaluate((floorId) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
.__GUIDE_3D_VISUAL_STABILITY__
return api?.getFloorBaseline(floorId) || null
}, marker.floorId)
expect(baseline).not.toBeNull()
let modelRequestsAfterBaseline = 0
page.on('request', (request) => {
if (/\.(glb|gltf)(?:$|\?)/i.test(request.url())) modelRequestsAfterBaseline += 1
})
await page.mouse.click(marker.screen.x, marker.screen.y)
await expect.poll(async () => (await getReport(page)).activeFocusPoiId, { timeout: 10_000 }).toBe(marker.poiId)
await expect(page.locator('.guide-poi-card')).toBeVisible()
if (kind === 'facility') {
await page.locator('.poi-card-collapsed').click()
}
await page.locator('.poi-card-close').click()
await expect(page.locator('.guide-poi-card')).toBeHidden()
await expect.poll(async () => (await getReport(page)).activeFocusPoiId, { timeout: 10_000 }).toBe('')
await expect.poll(async () => (await getReport(page)).focus, { timeout: 10_000 }).toBeNull()
await expect.poll(async () => (await getReport(page)).hasPendingTargetFocus, { timeout: 10_000 }).toBe(false)
await expect.poll(async () => (await getReport(page)).isCameraTweening, { timeout: 10_000 }).toBe(false)
const afterClose = await getReport(page)
expect(afterClose.activeView).toBe('floor')
expect(afterClose.floorId).toBe(marker.floorId)
expect(getMaximumCameraDelta(baseline!, afterClose.camera)).toBeLessThan(1e-6)
expect(modelRequestsAfterBaseline).toBe(0)
await page.waitForTimeout(300)
const afterIdle = await getReport(page)
expect(getMaximumCameraDelta(afterClose.camera, afterIdle.camera)).toBeLessThan(1e-6)
}
})
})