This commit is contained in:
@@ -209,6 +209,7 @@ interface LoadFloorOptions {
|
||||
detachPoiBeforeLoad?: boolean
|
||||
allowSameFloorReload?: boolean
|
||||
cameraSnapshot?: CameraSnapshot
|
||||
applyFloorBaseline?: boolean
|
||||
onCameraStable?: () => void
|
||||
}
|
||||
|
||||
@@ -243,6 +244,21 @@ interface CameraSnapshot {
|
||||
distance: number
|
||||
}
|
||||
|
||||
type ResetViewBaselineOptions = {
|
||||
view: 'overview' | 'floor'
|
||||
floorId?: string
|
||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||
}
|
||||
|
||||
interface FloorViewBaseline {
|
||||
floorId: string
|
||||
modelUrl: string
|
||||
packageEpoch: number
|
||||
aspectRatio: number
|
||||
boundsSignature: string
|
||||
camera: CameraSnapshot
|
||||
}
|
||||
|
||||
// 所有普通楼层/外观切换共享该外观基准;切换时使用完整快照恢复而非包围盒拟合。
|
||||
const referenceOverviewCameraState: CameraSnapshot = (() => {
|
||||
const referenceCamera = new THREE.PerspectiveCamera(
|
||||
@@ -517,6 +533,8 @@ let firstModelLoadStartedAt = 0
|
||||
let firstModelVisibleReported = false
|
||||
let initialModelSettled = false
|
||||
let initialGuideState: { floorId: string; camera: CameraSnapshot } | null = null
|
||||
const floorViewBaselines = new Map<string, FloorViewBaseline>()
|
||||
let modelPackageEpoch = 0
|
||||
|
||||
const cameraTweenEnabled = true
|
||||
const cameraTweenDurationMs = 480
|
||||
@@ -1983,7 +2001,9 @@ const handleResize = () => {
|
||||
|
||||
const width = Math.max(1, container.clientWidth)
|
||||
const height = Math.max(1, container.clientHeight)
|
||||
camera.aspect = width / height
|
||||
const nextAspect = width / height
|
||||
if (Math.abs(camera.aspect - nextAspect) > 1e-6) clearFloorViewBaselines()
|
||||
camera.aspect = nextAspect
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(width, height)
|
||||
}
|
||||
@@ -3390,6 +3410,99 @@ const applyReferenceOverviewCameraState = () => {
|
||||
restoreCameraSnapshot(referenceOverviewCameraState)
|
||||
}
|
||||
|
||||
const clearFloorViewBaselines = () => {
|
||||
floorViewBaselines.clear()
|
||||
}
|
||||
|
||||
const getBoxSignature = (box: THREE.Box3) => [
|
||||
box.min.x,
|
||||
box.min.y,
|
||||
box.min.z,
|
||||
box.max.x,
|
||||
box.max.y,
|
||||
box.max.z
|
||||
].map((value) => value.toFixed(6)).join(':')
|
||||
|
||||
const createFloorViewBaseline = (floorId: string, model: THREE.Object3D, modelUrl: string): FloorViewBaseline | null => {
|
||||
if (!camera || !controls) return null
|
||||
|
||||
const overviewCamera = initialGuideState?.camera || referenceOverviewCameraState
|
||||
const box = getObjectBox(model)
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
const maxDim = Math.max(size.x, size.y, size.z, 1)
|
||||
const verticalFov = overviewCamera.fov * (Math.PI / 180)
|
||||
const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * camera.aspect)
|
||||
const visibleFraction = FLOOR_CAMERA_DISTANCE_FACTOR
|
||||
const distance = Math.max(
|
||||
size.y / (2 * Math.tan(verticalFov / 2) * visibleFraction),
|
||||
size.x / (2 * Math.tan(horizontalFov / 2) * visibleFraction),
|
||||
size.z / (2 * Math.tan(verticalFov / 2) * visibleFraction),
|
||||
1
|
||||
)
|
||||
const direction = overviewCamera.position.clone().sub(overviewCamera.target).normalize()
|
||||
const target = center.clone()
|
||||
const position = target.clone().add(direction.multiplyScalar(distance))
|
||||
const offsetRatio = SGS_VISUAL_RENDER_CONFIG.framing.floorScreenOffsetRatio
|
||||
const viewDirection = target.clone().sub(position).normalize()
|
||||
const cameraRight = new THREE.Vector3().crossVectors(viewDirection, overviewCamera.up).normalize()
|
||||
const viewHeight = 2 * distance * Math.tan(verticalFov / 2)
|
||||
const viewWidth = viewHeight * camera.aspect
|
||||
const screenOffset = cameraRight.multiplyScalar(offsetRatio.x * viewWidth)
|
||||
.add(overviewCamera.up.clone().multiplyScalar(offsetRatio.y * viewHeight))
|
||||
position.add(screenOffset)
|
||||
target.add(screenOffset)
|
||||
|
||||
const baselineCamera = new THREE.PerspectiveCamera(
|
||||
overviewCamera.fov,
|
||||
camera.aspect,
|
||||
Math.max(SGS_VISUAL_RENDER_CONFIG.camera.near, maxDim / 1000),
|
||||
Math.max(SGS_VISUAL_RENDER_CONFIG.camera.far, maxDim * 20)
|
||||
)
|
||||
baselineCamera.position.copy(position)
|
||||
baselineCamera.up.copy(overviewCamera.up)
|
||||
baselineCamera.zoom = overviewCamera.zoom
|
||||
baselineCamera.lookAt(target)
|
||||
baselineCamera.updateProjectionMatrix()
|
||||
|
||||
return {
|
||||
floorId,
|
||||
modelUrl,
|
||||
packageEpoch: modelPackageEpoch,
|
||||
aspectRatio: camera.aspect,
|
||||
boundsSignature: getBoxSignature(box),
|
||||
camera: {
|
||||
position: baselineCamera.position.clone(),
|
||||
target,
|
||||
up: baselineCamera.up.clone(),
|
||||
quaternion: baselineCamera.quaternion.clone(),
|
||||
fov: baselineCamera.fov,
|
||||
zoom: baselineCamera.zoom,
|
||||
near: baselineCamera.near,
|
||||
far: baselineCamera.far,
|
||||
distance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getFloorViewBaseline = (floorId: string, model: THREE.Object3D, modelUrl: string) => {
|
||||
const cached = floorViewBaselines.get(floorId)
|
||||
const boundsSignature = getBoxSignature(getObjectBox(model))
|
||||
if (
|
||||
cached
|
||||
&& cached.modelUrl === modelUrl
|
||||
&& cached.packageEpoch === modelPackageEpoch
|
||||
&& cached.boundsSignature === boundsSignature
|
||||
&& Math.abs(cached.aspectRatio - (camera?.aspect || 1)) < 1e-6
|
||||
) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const baseline = createFloorViewBaseline(floorId, model, modelUrl)
|
||||
if (baseline) floorViewBaselines.set(floorId, baseline)
|
||||
return baseline || null
|
||||
}
|
||||
|
||||
const serializeCameraSnapshot = (snapshot: CameraSnapshot) => ({
|
||||
position: { x: snapshot.position.x, y: snapshot.position.y, z: snapshot.position.z },
|
||||
target: { x: snapshot.target.x, y: snapshot.target.y, z: snapshot.target.z },
|
||||
@@ -3542,6 +3655,10 @@ const installVisualStabilityDiagnostics = () => {
|
||||
label: floor.label,
|
||||
modelMatchKeys: floor.modelMatchKeys || []
|
||||
})),
|
||||
getFloorBaseline: (floorId: string) => {
|
||||
const baseline = floorViewBaselines.get(floorId)
|
||||
return baseline ? serializeCameraSnapshot(baseline.camera) : null
|
||||
},
|
||||
switchFloor: handleFloorChange,
|
||||
showOverview,
|
||||
resetCamera,
|
||||
@@ -3563,6 +3680,7 @@ const installVisualStabilityDiagnostics = () => {
|
||||
return targetFocusQueue
|
||||
},
|
||||
clearTargetFocus,
|
||||
resetToViewBaseline,
|
||||
resetToInitialState
|
||||
}
|
||||
}
|
||||
@@ -4003,7 +4121,10 @@ const commitPreparedFloorScene = (
|
||||
selectedPOI.value = null
|
||||
updatePoiMarkerFocus()
|
||||
hasLoadedFloorViewOnce = true
|
||||
const cameraSnapshot = options.cameraSnapshot || referenceOverviewCameraState
|
||||
const floorBaseline = options.applyFloorBaseline
|
||||
? getFloorViewBaseline(expectedFloorId, activeModel, prepared.floor.modelUrl)
|
||||
: null
|
||||
const cameraSnapshot = floorBaseline?.camera || options.cameraSnapshot || referenceOverviewCameraState
|
||||
restoreCameraSnapshot(cameraSnapshot)
|
||||
if (!referenceBuildingAnchors.length) {
|
||||
updateReferenceBuildingAnchors(activeModel)
|
||||
@@ -5272,6 +5393,8 @@ const queueTargetFocus = (request: TargetPoiFocusRequest | null) => {
|
||||
const loadModelPackage = async () => {
|
||||
setProgress(8, '正在读取馆内导览资源...')
|
||||
const packageData = await props.modelSource.loadPackage()
|
||||
modelPackageEpoch += 1
|
||||
clearFloorViewBaselines()
|
||||
renderPackage.value = packageData
|
||||
|
||||
setProgress(14, '正在读取楼层索引...')
|
||||
@@ -5314,6 +5437,7 @@ const loadModelPackage = async () => {
|
||||
const init3DScene = async () => {
|
||||
try {
|
||||
disposeScene()
|
||||
clearFloorViewBaselines()
|
||||
isDisposed = false
|
||||
firstModelLoadStartedAt = getNow()
|
||||
firstModelVisibleReported = false
|
||||
@@ -5461,7 +5585,7 @@ const showMultiFloor = async () => {
|
||||
|
||||
// Restores business and camera state on the existing scene. It never initializes WebGL or
|
||||
// reloads the model package, so closing a detail page cannot remount ThreeMap.
|
||||
const resetToInitialState = async () => {
|
||||
const resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
|
||||
clearTargetFocus()
|
||||
invalidateModelLoads()
|
||||
adjacentPreloadSeq += 1
|
||||
@@ -5476,7 +5600,7 @@ const resetToInitialState = async () => {
|
||||
disposeFocusBase()
|
||||
clearFocusHallHighlight()
|
||||
updatePoiMarkerFocus()
|
||||
autoSwitchStateMachine.reset('overview')
|
||||
autoSwitchStateMachine.reset(options.view)
|
||||
resetInteractionGateState()
|
||||
|
||||
const baseline = initialGuideState
|
||||
@@ -5484,15 +5608,38 @@ const resetToInitialState = async () => {
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
currentFloor.value = baseline.floorId
|
||||
if (activeView.value !== 'overview') {
|
||||
await loadOverview({ cameraSnapshot: baseline.camera })
|
||||
if (options.view === 'floor') {
|
||||
const floorId = options.floorId
|
||||
if (!floorId) return false
|
||||
const floor = floorIndex.value.find((item) => item.floorId === floorId)
|
||||
if (!floor) return false
|
||||
|
||||
if (activeView.value !== 'floor' || currentFloor.value !== floorId || activeModel?.userData.floorId !== floorId) {
|
||||
await loadFloor(floorId, { applyFloorBaseline: true, detachPoiBeforeLoad: true })
|
||||
} else {
|
||||
const floorBaseline = getFloorViewBaseline(floorId, activeModel, floor.modelUrl)
|
||||
if (!floorBaseline) return false
|
||||
restoreCameraSnapshot(floorBaseline.camera)
|
||||
}
|
||||
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return false
|
||||
activeView.value = 'floor'
|
||||
currentFloor.value = floorId
|
||||
autoSwitchStateMachine.reset('floor')
|
||||
} else {
|
||||
currentFloor.value = baseline.floorId
|
||||
if (activeView.value !== 'overview') {
|
||||
await loadOverview({ cameraSnapshot: baseline.camera })
|
||||
}
|
||||
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return false
|
||||
restoreCameraSnapshot(baseline.camera)
|
||||
currentFloor.value = baseline.floorId
|
||||
activeView.value = 'overview'
|
||||
autoSwitchStateMachine.reset('overview')
|
||||
}
|
||||
if (controls && activeView.value === 'floor') {
|
||||
ensureFloorAutoExitZoomRange()
|
||||
autoSwitchStateMachine.setFloorInitialDistance(controls.getDistance())
|
||||
}
|
||||
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return false
|
||||
restoreCameraSnapshot(baseline.camera)
|
||||
currentFloor.value = baseline.floorId
|
||||
activeView.value = 'overview'
|
||||
autoSwitchStateMachine.reset('overview')
|
||||
refreshPoiVisibilityByDistance()
|
||||
return true
|
||||
} catch (error) {
|
||||
@@ -5505,6 +5652,11 @@ const resetToInitialState = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const resetToInitialState = () => resetToViewBaseline({
|
||||
view: 'overview',
|
||||
reason: 'manual-reset'
|
||||
})
|
||||
|
||||
const retryLoad = () => {
|
||||
init3DScene()
|
||||
}
|
||||
@@ -5595,6 +5747,7 @@ defineExpose({
|
||||
},
|
||||
clearSelection: clearMapSelection,
|
||||
clearRoute: clearRoutePreview,
|
||||
resetToViewBaseline,
|
||||
resetToInitialState,
|
||||
clearNavigation: () => clearMapSelection(false),
|
||||
disableAutoSwitchTemporarily
|
||||
|
||||
@@ -460,6 +460,11 @@ const indoorRendererRef = ref<{
|
||||
zoomCamera?: (direction: 'in' | 'out', options?: { source?: 'button' | 'gesture' }) => void
|
||||
clearSelection?: (shouldEmit?: boolean) => void
|
||||
clearRoute?: () => Promise<void> | void
|
||||
resetToViewBaseline?: (options: {
|
||||
view: 'overview' | 'floor'
|
||||
floorId?: string
|
||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||
}) => Promise<boolean> | boolean
|
||||
resetToInitialState?: () => Promise<boolean> | boolean
|
||||
disableAutoSwitchTemporarily?: (durationMs: number) => void
|
||||
} | null>(null)
|
||||
@@ -781,6 +786,11 @@ defineExpose({
|
||||
indoorRendererRef.value?.clearRoute?.()
|
||||
},
|
||||
showOverview: handleShowOverview,
|
||||
resetToViewBaseline: (options: {
|
||||
view: 'overview' | 'floor'
|
||||
floorId?: string
|
||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||
}) => indoorRendererRef.value?.resetToViewBaseline?.(options) || false,
|
||||
resetToInitialState: () => indoorRendererRef.value?.resetToInitialState?.() || false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -18,6 +18,12 @@ export type GuideModelInitialState = Readonly<{
|
||||
autoFloorSwitchActive: boolean
|
||||
}>
|
||||
|
||||
export type PoiDetailReturnContext = Readonly<{
|
||||
floorId: string
|
||||
resetMode: 'floor-baseline'
|
||||
requestId: number
|
||||
}>
|
||||
|
||||
// The object is frozen so callers cannot accidentally turn a captured baseline into live state.
|
||||
export const GUIDE_MODEL_INITIAL_STATE: GuideModelInitialState = Object.freeze({
|
||||
mode: '3d',
|
||||
@@ -38,8 +44,9 @@ export const GUIDE_MODEL_INITIAL_STATE: GuideModelInitialState = Object.freeze({
|
||||
})
|
||||
|
||||
const initialState = ref<GuideModelInitialState>(GUIDE_MODEL_INITIAL_STATE)
|
||||
const pendingModelResetAfterPoiDetail = ref(false)
|
||||
const pendingModelResetAfterPoiDetail = ref<PoiDetailReturnContext | null>(null)
|
||||
const requestGeneration = ref(0)
|
||||
let poiDetailRequestId = 0
|
||||
|
||||
export const useGuideModelState = () => {
|
||||
const initializeModel = (floorId: string) => {
|
||||
@@ -52,18 +59,24 @@ export const useGuideModelState = () => {
|
||||
return initialState.value
|
||||
}
|
||||
|
||||
const beginPoiDetailReturn = () => {
|
||||
pendingModelResetAfterPoiDetail.value = true
|
||||
const beginPoiDetailReturn = (floorId: string) => {
|
||||
const context = Object.freeze({
|
||||
floorId,
|
||||
resetMode: 'floor-baseline' as const,
|
||||
requestId: ++poiDetailRequestId
|
||||
})
|
||||
pendingModelResetAfterPoiDetail.value = context
|
||||
return context
|
||||
}
|
||||
|
||||
const cancelPoiDetailReturn = () => {
|
||||
pendingModelResetAfterPoiDetail.value = false
|
||||
pendingModelResetAfterPoiDetail.value = null
|
||||
}
|
||||
|
||||
const consumePoiDetailReturn = () => {
|
||||
if (!pendingModelResetAfterPoiDetail.value) return false
|
||||
pendingModelResetAfterPoiDetail.value = false
|
||||
return true
|
||||
const context = pendingModelResetAfterPoiDetail.value
|
||||
pendingModelResetAfterPoiDetail.value = null
|
||||
return context
|
||||
}
|
||||
|
||||
// All async focus/floor operations compare this generation before committing.
|
||||
|
||||
@@ -327,7 +327,7 @@ import type {
|
||||
import {
|
||||
type TargetPoiFocusRequestViewModel
|
||||
} from '@/view-models/guideViewModels'
|
||||
import { useGuideModelState } from '@/composables/useGuideModelState'
|
||||
import { useGuideModelState, type PoiDetailReturnContext } from '@/composables/useGuideModelState'
|
||||
import {
|
||||
ARRIVAL_TARGETS_BY_TYPE,
|
||||
type ArrivalSearchTarget,
|
||||
@@ -418,6 +418,11 @@ const returningToOverview = ref(false)
|
||||
const guideMapShellRef = ref<{
|
||||
clearRoute?: () => void
|
||||
showOverview?: () => Promise<void> | void
|
||||
resetToViewBaseline?: (options: {
|
||||
view: 'overview' | 'floor'
|
||||
floorId?: string
|
||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||
}) => Promise<boolean> | boolean
|
||||
resetToInitialState?: () => Promise<boolean> | boolean
|
||||
} | null>(null)
|
||||
const homeSearchPanelRef = ref<{
|
||||
@@ -430,7 +435,7 @@ const homeSearchExpanded = ref(false)
|
||||
const homeCategoryModeActive = ref(false)
|
||||
const searchVisiblePoiIds = ref<string[] | null>(null)
|
||||
const searchTargetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
|
||||
let pendingGuideModelResetUntilReady = false
|
||||
let pendingGuideModelResetUntilReady: PoiDetailReturnContext | null = null
|
||||
|
||||
type PoiSearchOverlayHistoryState = {
|
||||
museumGuideOverlay?: 'poi-search'
|
||||
@@ -1036,8 +1041,9 @@ const handleInitialModelReady = (event: { view: GuideIndoorView; floorId?: strin
|
||||
guideModelState.initializeModel(event.floorId || activeGuideFloor.value)
|
||||
settleLaunchOverlayByModel('ready', event)
|
||||
if (pendingGuideModelResetUntilReady) {
|
||||
pendingGuideModelResetUntilReady = false
|
||||
void resetGuideModelToInitial({ reason: 'queued-poi-detail-close' })
|
||||
const context = pendingGuideModelResetUntilReady
|
||||
pendingGuideModelResetUntilReady = null
|
||||
void resetGuideModelToInitial({ reason: 'queued-poi-detail-close', floorId: context.floorId })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1456,7 +1462,13 @@ const closeHomeSearchDock = () => {
|
||||
homeSearchExpanded.value = false
|
||||
}
|
||||
|
||||
const resetGuideModelToInitial = async ({ reason }: { reason: 'poi-detail-close' | 'queued-poi-detail-close' }) => {
|
||||
const resetGuideModelToInitial = async ({
|
||||
reason,
|
||||
floorId
|
||||
}: {
|
||||
reason: 'poi-detail-close' | 'queued-poi-detail-close'
|
||||
floorId: string
|
||||
}) => {
|
||||
const initial = guideModelState.initialState.value
|
||||
guideModelState.invalidatePendingRequests()
|
||||
searchTargetFocusRequest.value = null
|
||||
@@ -1476,25 +1488,34 @@ const resetGuideModelToInitial = async ({ reason }: { reason: 'poi-detail-close'
|
||||
failedFloorId.value = ''
|
||||
is3DMode.value = true
|
||||
guideOutdoorState.value = 'home'
|
||||
indoorView.value = initial.indoorView
|
||||
activeGuideFloor.value = initial.defaultActiveFloorId || activeGuideFloor.value
|
||||
renderedFloorId.value = initial.loadedFloorId || activeGuideFloor.value
|
||||
indoorView.value = 'floor'
|
||||
activeGuideFloor.value = floorId || initial.defaultActiveFloorId || activeGuideFloor.value
|
||||
renderedFloorId.value = activeGuideFloor.value
|
||||
closeArrivalPanel()
|
||||
await homeSearchPanelRef.value?.resetSearchState?.()
|
||||
closeHomeSearchDock()
|
||||
|
||||
const resetApplied = await guideMapShellRef.value?.resetToInitialState?.()
|
||||
const resetApplied = await guideMapShellRef.value?.resetToViewBaseline?.({
|
||||
view: 'floor',
|
||||
floorId: activeGuideFloor.value,
|
||||
reason: 'poi-detail-close'
|
||||
})
|
||||
if (!resetApplied) {
|
||||
// The renderer is still becoming ready; retain only the latest requested reset.
|
||||
pendingGuideModelResetUntilReady = true
|
||||
pendingGuideModelResetUntilReady = Object.freeze({
|
||||
floorId: activeGuideFloor.value,
|
||||
resetMode: 'floor-baseline',
|
||||
requestId: guideModelState.invalidatePendingRequests()
|
||||
})
|
||||
}
|
||||
console.info('馆内三维模型已恢复统一初始状态:', { reason })
|
||||
}
|
||||
|
||||
onShow(async () => {
|
||||
await nextTick()
|
||||
if (guideModelState.consumePoiDetailReturn()) {
|
||||
await resetGuideModelToInitial({ reason: 'poi-detail-close' })
|
||||
const detailReturnContext = guideModelState.consumePoiDetailReturn()
|
||||
if (detailReturnContext) {
|
||||
await resetGuideModelToInitial({ reason: 'poi-detail-close', floorId: detailReturnContext.floorId })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1596,7 +1617,7 @@ const createSearchDetailUrl = (poi: MuseumPoi, context: PoiSearchContext) => {
|
||||
const handleHomeSearchResultTap = async (poi: MuseumPoi, context: PoiSearchContext) => {
|
||||
// The homepage keeps its renderer mounted; detail close is consumed by onShow once.
|
||||
searchTargetFocusRequest.value = null
|
||||
guideModelState.beginPoiDetailReturn()
|
||||
guideModelState.beginPoiDetailReturn(poi.floorId)
|
||||
uni.navigateTo({
|
||||
url: createSearchDetailUrl(poi, context),
|
||||
fail: () => {
|
||||
|
||||
@@ -60,7 +60,13 @@ interface VisualStabilityApi {
|
||||
getFloors: () => FloorReport[]
|
||||
switchFloor: (floorId: string) => Promise<void>
|
||||
showOverview: () => Promise<void>
|
||||
resetToViewBaseline: (options: {
|
||||
view: 'overview' | 'floor'
|
||||
floorId?: string
|
||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||
}) => Promise<boolean>
|
||||
resetToInitialState: () => Promise<boolean>
|
||||
getFloorBaseline: (floorId: string) => CameraReport | null
|
||||
getFloorPois: (floorId: string) => Promise<PoiFocusCandidate[]>
|
||||
focusTargetPoi: (request: {
|
||||
requestId: number | string
|
||||
@@ -432,3 +438,34 @@ test('reset invalidates an in-flight cross-floor focus before it can overwrite t
|
||||
expect(report.hasPendingTargetFocus).toBe(false)
|
||||
expect(report.focus).toBeNull()
|
||||
})
|
||||
|
||||
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 expect.poll(async () => (await getApiState(page))?.floors.length ?? 0, { timeout: 180_000 }).toBeGreaterThan(0)
|
||||
const state = await getApiState(page)
|
||||
expect(state).not.toBeNull()
|
||||
|
||||
for (const floor of state!.floors) {
|
||||
await page.evaluate(async (floorId) => {
|
||||
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, reason: 'floor-reset' })
|
||||
}, floor.floorId)
|
||||
const first = await getReport(page)
|
||||
await page.evaluate(async (floorId) => {
|
||||
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, reason: 'floor-reset' })
|
||||
}, floor.floorId)
|
||||
const second = await getReport(page)
|
||||
expect(second.activeView).toBe('floor')
|
||||
expect(second.floorId).toBe(floor.floorId)
|
||||
expect(getMaximumCameraDelta(first.camera, second.camera)).toBeLessThan(1e-6)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -321,7 +321,7 @@ describe('首页搜索与地图闭环', () => {
|
||||
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
|
||||
})
|
||||
|
||||
it('分类结果进入详情后统一恢复首页模型初始状态', async () => {
|
||||
it('分类结果进入详情后恢复目标楼层标准视图', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
@@ -356,9 +356,14 @@ describe('首页搜索与地图闭环', () => {
|
||||
|
||||
expect(testState.searchResetCount).toBeGreaterThan(0)
|
||||
expect(testState.searchRestoreCount).toBe(0)
|
||||
expect(map.props('activeFloor')).toBe('L1')
|
||||
expect(map.props('activeFloor')).toBe('L2')
|
||||
expect(map.props('visiblePoiIds')).toBeNull()
|
||||
expect(map.props('targetFocusRequest')).toBeNull()
|
||||
|
||||
const resetCount = testState.searchResetCount
|
||||
await testState.onShowHandler?.()
|
||||
await flushPromises()
|
||||
expect(testState.searchResetCount).toBe(resetCount)
|
||||
})
|
||||
|
||||
it('全屏搜索结果进入详情并返回后同样恢复为默认收缩态', async () => {
|
||||
@@ -380,6 +385,7 @@ describe('首页搜索与地图闭环', () => {
|
||||
expect(testState.searchRestoreCount).toBe(0)
|
||||
expect(map.props('visiblePoiIds')).toBeNull()
|
||||
expect(map.props('targetFocusRequest')).toBeNull()
|
||||
expect(map.props('activeFloor')).toBe('L2')
|
||||
})
|
||||
|
||||
it('页面重新显示时要求搜索面板恢复原列表位置', async () => {
|
||||
|
||||
Reference in New Issue
Block a user