This commit is contained in:
@@ -3430,6 +3430,8 @@ const restoreCameraSnapshot = (snapshot: CameraSnapshot) => {
|
||||
}
|
||||
}
|
||||
|
||||
export type ResetViewBaselineResult = 'applied' | 'not-ready' | 'invalid-target' | 'failed' | 'stale'
|
||||
|
||||
const applyReferenceOverviewCameraState = () => {
|
||||
restoreCameraSnapshot(referenceOverviewCameraState)
|
||||
}
|
||||
@@ -3652,6 +3654,49 @@ const getVisualFocusReport = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getVisiblePoiScreenPositions = () => {
|
||||
if (!renderer || !camera) return []
|
||||
|
||||
const rect = renderer.domElement.getBoundingClientRect()
|
||||
return getPoiSprites().flatMap((sprite) => {
|
||||
const poi = sprite.userData.poi as RenderPoi | undefined
|
||||
const screen = getProjectedScreenPosition(sprite)
|
||||
let ancestor: THREE.Object3D | null = sprite
|
||||
while (ancestor) {
|
||||
if (!ancestor.visible) return []
|
||||
ancestor = ancestor.parent
|
||||
}
|
||||
if (
|
||||
!poi
|
||||
|| !screen
|
||||
|| screen.x < 0
|
||||
|| screen.x > rect.width
|
||||
|| screen.y < 0
|
||||
|| screen.y > rect.height
|
||||
) return []
|
||||
|
||||
// Match the production fallback hit-test so diagnostics only expose coordinates
|
||||
// that a tap can resolve to this exact current marker.
|
||||
const hit = findNearestPoiMarkerByScreenPoint({
|
||||
clientX: rect.left + screen.x,
|
||||
clientY: rect.top + screen.y
|
||||
} as PointerEvent, rect)
|
||||
if (hit !== sprite) return []
|
||||
|
||||
return [{
|
||||
poiId: poi.id,
|
||||
floorId: poi.floorId,
|
||||
name: poi.name,
|
||||
kind: poi.kind,
|
||||
primaryCategory: poi.primaryCategory,
|
||||
screen: {
|
||||
x: rect.left + screen.x,
|
||||
y: rect.top + screen.y
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
const getVisualStabilityReport = () => ({
|
||||
camera: serializeCameraSnapshot(captureCameraSnapshot()),
|
||||
modelRoot: getModelRootTransformAudit(activeModel),
|
||||
@@ -3674,6 +3719,8 @@ const installVisualStabilityDiagnostics = () => {
|
||||
}
|
||||
diagnosticsWindow.__GUIDE_3D_VISUAL_STABILITY__ = {
|
||||
getReport: getVisualStabilityReport,
|
||||
isInitialModelReady: () => initialModelSettled && Boolean(initialGuideState && activeModel),
|
||||
getVisiblePoiScreenPositions,
|
||||
getFloors: () => floorIndex.value.map((floor) => ({
|
||||
floorId: floor.floorId,
|
||||
label: floor.label,
|
||||
@@ -3696,6 +3743,8 @@ const installVisualStabilityDiagnostics = () => {
|
||||
poiId: poi.id,
|
||||
floorId: poi.floorId,
|
||||
name: poi.name,
|
||||
kind: poi.kind,
|
||||
primaryCategory: poi.primaryCategory,
|
||||
positionGltf: poi.positionGltf!
|
||||
}))
|
||||
},
|
||||
@@ -5610,6 +5659,18 @@ 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 resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
|
||||
// Do not invalidate the first model load while the renderer is still being initialized.
|
||||
// The page-level guide model state will replay only its latest not-ready request.
|
||||
const baseline = initialGuideState
|
||||
if (!baseline || !scene || !camera || !controls) return 'not-ready' as const
|
||||
if (loadError.value) return 'failed' as const
|
||||
|
||||
if (options.view === 'floor') {
|
||||
if (!options.floorId || !floorIndex.value.some((item) => item.floorId === options.floorId)) {
|
||||
return 'invalid-target' as const
|
||||
}
|
||||
}
|
||||
|
||||
clearTargetFocus()
|
||||
invalidateModelLoads()
|
||||
adjacentPreloadSeq += 1
|
||||
@@ -5627,25 +5688,22 @@ const resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
|
||||
autoSwitchStateMachine.reset(options.view)
|
||||
resetInteractionGateState()
|
||||
|
||||
const baseline = initialGuideState
|
||||
if (!baseline || !scene || !camera || !controls || loadError.value) return false
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
if (options.view === 'floor') {
|
||||
const floorId = options.floorId
|
||||
if (!floorId) return false
|
||||
if (!floorId) return 'invalid-target' as const
|
||||
const floor = floorIndex.value.find((item) => item.floorId === floorId)
|
||||
if (!floor) return false
|
||||
if (!floor) return 'invalid-target' as const
|
||||
|
||||
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
|
||||
if (!floorBaseline) return 'failed' as const
|
||||
restoreCameraSnapshot(floorBaseline.camera)
|
||||
}
|
||||
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return false
|
||||
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return 'stale' as const
|
||||
activeView.value = 'floor'
|
||||
currentFloor.value = floorId
|
||||
autoSwitchStateMachine.reset('floor')
|
||||
@@ -5654,7 +5712,7 @@ const resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
|
||||
if (activeView.value !== 'overview') {
|
||||
await loadOverview({ cameraSnapshot: baseline.camera })
|
||||
}
|
||||
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return false
|
||||
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return 'stale' as const
|
||||
restoreCameraSnapshot(baseline.camera)
|
||||
currentFloor.value = baseline.floorId
|
||||
activeView.value = 'overview'
|
||||
@@ -5665,12 +5723,13 @@ const resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
|
||||
autoSwitchStateMachine.setFloorInitialDistance(controls.getDistance())
|
||||
}
|
||||
refreshPoiVisibilityByDistance()
|
||||
return true
|
||||
return 'applied' as const
|
||||
} catch (error) {
|
||||
if (isStaleModelLoadError(error)) return 'stale' as const
|
||||
if (!isStaleModelLoadError(error)) {
|
||||
console.error('恢复馆内导览初始状态失败:', error)
|
||||
}
|
||||
return false
|
||||
return 'failed' as const
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
@@ -240,6 +240,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import TencentMap from '@/components/map/TencentMap.vue'
|
||||
// #ifdef H5
|
||||
import ThreeMap from '@/components/map/ThreeMap.vue'
|
||||
import type { ResetViewBaselineResult } from '@/components/map/ThreeMap.vue'
|
||||
// #endif
|
||||
import type {
|
||||
GuideModelSource,
|
||||
@@ -464,8 +465,8 @@ const indoorRendererRef = ref<{
|
||||
view: 'overview' | 'floor'
|
||||
floorId?: string
|
||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||
}) => Promise<boolean> | boolean
|
||||
resetToInitialState?: () => Promise<boolean> | boolean
|
||||
}) => Promise<ResetViewBaselineResult> | ResetViewBaselineResult
|
||||
resetToInitialState?: () => Promise<ResetViewBaselineResult> | ResetViewBaselineResult
|
||||
disableAutoSwitchTemporarily?: (durationMs: number) => void
|
||||
} | null>(null)
|
||||
|
||||
@@ -790,8 +791,8 @@ defineExpose({
|
||||
view: 'overview' | 'floor'
|
||||
floorId?: string
|
||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||
}) => indoorRendererRef.value?.resetToViewBaseline?.(options) || false,
|
||||
resetToInitialState: () => indoorRendererRef.value?.resetToInitialState?.() || false
|
||||
}) => indoorRendererRef.value?.resetToViewBaseline?.(options) || 'not-ready',
|
||||
resetToInitialState: () => indoorRendererRef.value?.resetToInitialState?.() || 'not-ready'
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -24,6 +24,14 @@ export type PoiDetailReturnContext = Readonly<{
|
||||
requestId: number
|
||||
}>
|
||||
|
||||
export type GuideModelResetReason = 'poi-detail-close' | 'floor-reset'
|
||||
|
||||
export type GuideModelResetRequest = Readonly<{
|
||||
floorId: string
|
||||
requestId: number
|
||||
rendererReason: GuideModelResetReason
|
||||
}>
|
||||
|
||||
type GuideModelReadyState = Readonly<{
|
||||
view: 'overview' | 'floor' | 'multi'
|
||||
floorId: string
|
||||
@@ -52,6 +60,7 @@ const initialState = ref<GuideModelInitialState>(GUIDE_MODEL_INITIAL_STATE)
|
||||
const pendingModelResetAfterPoiDetail = ref<PoiDetailReturnContext | null>(null)
|
||||
const confirmedPoiDetailRequestId = ref<number | null>(null)
|
||||
const requestGeneration = ref(0)
|
||||
const pendingViewBaselineReset = ref<GuideModelResetRequest | null>(null)
|
||||
let poiDetailRequestId = 0
|
||||
|
||||
export const useGuideModelState = () => {
|
||||
@@ -107,15 +116,53 @@ export const useGuideModelState = () => {
|
||||
return requestGeneration.value
|
||||
}
|
||||
|
||||
const beginViewBaselineReset = ({ floorId, rendererReason }: {
|
||||
floorId: string
|
||||
rendererReason: GuideModelResetReason
|
||||
}) => Object.freeze({
|
||||
floorId,
|
||||
rendererReason,
|
||||
requestId: invalidatePendingRequests()
|
||||
})
|
||||
|
||||
const isCurrentRequest = (requestId: number) => requestGeneration.value === requestId
|
||||
|
||||
// The renderer may not yet exist while the page has already received a return event.
|
||||
// Keep exactly one latest request here so page lifecycle hooks cannot race each other.
|
||||
const queueViewBaselineReset = (request: GuideModelResetRequest) => {
|
||||
if (!isCurrentRequest(request.requestId)) return false
|
||||
pendingViewBaselineReset.value = request
|
||||
return true
|
||||
}
|
||||
|
||||
const completeViewBaselineReset = (request: GuideModelResetRequest) => {
|
||||
if (!isCurrentRequest(request.requestId)) return false
|
||||
pendingViewBaselineReset.value = null
|
||||
return true
|
||||
}
|
||||
|
||||
const consumePendingViewBaselineReset = () => {
|
||||
const request = pendingViewBaselineReset.value
|
||||
if (!request || !isCurrentRequest(request.requestId)) return null
|
||||
pendingViewBaselineReset.value = null
|
||||
return request
|
||||
}
|
||||
|
||||
return {
|
||||
initialState: readonly(initialState),
|
||||
pendingModelResetAfterPoiDetail: readonly(pendingModelResetAfterPoiDetail),
|
||||
requestGeneration: readonly(requestGeneration),
|
||||
pendingViewBaselineReset: readonly(pendingViewBaselineReset),
|
||||
initializeModel,
|
||||
beginPoiDetailReturn,
|
||||
confirmPoiDetailReturn,
|
||||
cancelPoiDetailReturn,
|
||||
consumePoiDetailReturn,
|
||||
invalidatePendingRequests
|
||||
invalidatePendingRequests,
|
||||
beginViewBaselineReset,
|
||||
isCurrentRequest,
|
||||
queueViewBaselineReset,
|
||||
completeViewBaselineReset,
|
||||
consumePendingViewBaselineReset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<view class="poi-card-soft-action" @tap="collapsePoiCard">
|
||||
<text class="poi-card-soft-text">收起</text>
|
||||
</view>
|
||||
<view class="poi-card-close" @tap="handleGuideSelectionClear">
|
||||
<view class="poi-card-close" @tap="handleGuidePoiCardClose">
|
||||
<text class="poi-card-close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -327,7 +327,12 @@ import type {
|
||||
import {
|
||||
type TargetPoiFocusRequestViewModel
|
||||
} from '@/view-models/guideViewModels'
|
||||
import { useGuideModelState, type PoiDetailReturnContext } from '@/composables/useGuideModelState'
|
||||
import {
|
||||
useGuideModelState,
|
||||
type GuideModelResetReason,
|
||||
type GuideModelResetRequest
|
||||
} from '@/composables/useGuideModelState'
|
||||
import type { ResetViewBaselineResult } from '@/components/map/ThreeMap.vue'
|
||||
import {
|
||||
ARRIVAL_TARGETS_BY_TYPE,
|
||||
type ArrivalSearchTarget,
|
||||
@@ -422,7 +427,7 @@ const guideMapShellRef = ref<{
|
||||
view: 'overview' | 'floor'
|
||||
floorId?: string
|
||||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||||
}) => Promise<boolean> | boolean
|
||||
}) => Promise<ResetViewBaselineResult> | ResetViewBaselineResult
|
||||
resetToInitialState?: () => Promise<boolean> | boolean
|
||||
} | null>(null)
|
||||
const homeSearchPanelRef = ref<{
|
||||
@@ -435,8 +440,6 @@ const homeSearchExpanded = ref(false)
|
||||
const homeCategoryModeActive = ref(false)
|
||||
const searchVisiblePoiIds = ref<string[] | null>(null)
|
||||
const searchTargetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
|
||||
let pendingGuideModelResetUntilReady: PoiDetailReturnContext | null = null
|
||||
|
||||
type PoiSearchOverlayHistoryState = {
|
||||
museumGuideOverlay?: 'poi-search'
|
||||
[key: string]: unknown
|
||||
@@ -578,6 +581,14 @@ const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
|
||||
const indoorModelSource = guideUseCase.getModelSource()
|
||||
const guideFloors = ref<MuseumFloor[]>([])
|
||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||
const getPreferredInitialGuideFloorId = (floors: MuseumFloor[]) => (
|
||||
floors.find((floor) => (
|
||||
normalizeGuideFloorId(floor.id) === 'L1'
|
||||
|| normalizeGuideFloorId(floor.label) === 'L1'
|
||||
))?.id
|
||||
|| floors[0]?.id
|
||||
|| ''
|
||||
)
|
||||
const getGuideFloorById = (floorId: string) => (
|
||||
guideFloors.value.find((floor) => floor.id === floorId)
|
||||
)
|
||||
@@ -923,7 +934,7 @@ const loadGuideFloors = async () => {
|
||||
? normalizeGuideFloorId(activeGuideFloor.value)
|
||||
: ''
|
||||
activeGuideFloor.value = floors.find((floor) => floor.id === normalizedActiveFloorId)?.id
|
||||
|| floors[0]?.id
|
||||
|| getPreferredInitialGuideFloorId(floors)
|
||||
|| activeGuideFloor.value
|
||||
renderedFloorId.value = activeGuideFloor.value
|
||||
} catch (error) {
|
||||
@@ -1048,10 +1059,13 @@ const handleInitialModelReady = (event: { view: GuideIndoorView; floorId?: strin
|
||||
guideModelState.initializeModel({ view: event.view, floorId: readyFloorId })
|
||||
}
|
||||
settleLaunchOverlayByModel('ready', event)
|
||||
if (pendingGuideModelResetUntilReady) {
|
||||
const context = pendingGuideModelResetUntilReady
|
||||
pendingGuideModelResetUntilReady = null
|
||||
void resetGuideModelToInitial({ reason: 'queued-poi-detail-close', floorId: context.floorId })
|
||||
const context = guideModelState.consumePendingViewBaselineReset()
|
||||
if (context) {
|
||||
void resetGuideModelToFloorBaseline({
|
||||
rendererReason: context.rendererReason,
|
||||
floorId: context.floorId,
|
||||
request: context
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1086,6 +1100,19 @@ const handleGuideSelectionClear = () => {
|
||||
isPoiCardCollapsed.value = false
|
||||
}
|
||||
|
||||
const handleGuidePoiCardClose = () => {
|
||||
const floorId = selectedGuidePoi.value?.floorId
|
||||
if (!floorId) {
|
||||
handleGuideSelectionClear()
|
||||
return
|
||||
}
|
||||
|
||||
void resetGuideModelToFloorBaseline({
|
||||
rendererReason: 'floor-reset',
|
||||
floorId
|
||||
})
|
||||
}
|
||||
|
||||
const collapsePoiCard = () => {
|
||||
isPoiCardCollapsed.value = true
|
||||
}
|
||||
@@ -1507,15 +1534,17 @@ const closeHomeSearchDock = () => {
|
||||
homeSearchExpanded.value = false
|
||||
}
|
||||
|
||||
const resetGuideModelToInitial = async ({
|
||||
reason,
|
||||
floorId
|
||||
const resetGuideModelToFloorBaseline = async ({
|
||||
rendererReason,
|
||||
floorId,
|
||||
request
|
||||
}: {
|
||||
reason: 'poi-detail-close' | 'queued-poi-detail-close'
|
||||
rendererReason: GuideModelResetReason
|
||||
floorId: string
|
||||
request?: GuideModelResetRequest
|
||||
}) => {
|
||||
const initial = guideModelState.initialState.value
|
||||
const resetRequestId = guideModelState.invalidatePendingRequests()
|
||||
const resetRequest = request || guideModelState.beginViewBaselineReset({ floorId, rendererReason })
|
||||
searchTargetFocusRequest.value = null
|
||||
searchVisiblePoiIds.value = null
|
||||
homeCategoryModeActive.value = false
|
||||
@@ -1537,32 +1566,32 @@ const resetGuideModelToInitial = async ({
|
||||
activeGuideFloor.value = floorId || initial.defaultActiveFloorId || activeGuideFloor.value
|
||||
closeArrivalPanel()
|
||||
await homeSearchPanelRef.value?.resetSearchState?.()
|
||||
if (!guideModelState.isCurrentRequest(resetRequest.requestId)) return
|
||||
closeHomeSearchDock()
|
||||
|
||||
const resetApplied = await guideMapShellRef.value?.resetToViewBaseline?.({
|
||||
const resetResult = await guideMapShellRef.value?.resetToViewBaseline?.({
|
||||
view: 'floor',
|
||||
floorId: activeGuideFloor.value,
|
||||
reason: 'poi-detail-close'
|
||||
reason: rendererReason
|
||||
})
|
||||
if (resetApplied) {
|
||||
if (!guideModelState.isCurrentRequest(resetRequest.requestId)) return
|
||||
if (resetResult === 'applied') {
|
||||
renderedFloorId.value = activeGuideFloor.value
|
||||
pendingGuideModelResetUntilReady = null
|
||||
} else {
|
||||
// The renderer is still becoming ready; retain only the latest requested reset.
|
||||
pendingGuideModelResetUntilReady = Object.freeze({
|
||||
floorId: activeGuideFloor.value,
|
||||
resetMode: 'floor-baseline',
|
||||
requestId: resetRequestId
|
||||
})
|
||||
guideModelState.completeViewBaselineReset(resetRequest)
|
||||
console.info('馆内三维模型已恢复楼层视觉基线:', { rendererReason })
|
||||
} else if (resetResult === 'not-ready' || resetResult === undefined) {
|
||||
guideModelState.queueViewBaselineReset(resetRequest)
|
||||
}
|
||||
console.info('馆内三维模型已恢复统一初始状态:', { reason })
|
||||
}
|
||||
|
||||
onShow(async () => {
|
||||
await nextTick()
|
||||
const detailReturnContext = guideModelState.consumePoiDetailReturn()
|
||||
if (detailReturnContext) {
|
||||
await resetGuideModelToInitial({ reason: 'poi-detail-close', floorId: detailReturnContext.floorId })
|
||||
await resetGuideModelToFloorBaseline({
|
||||
rendererReason: 'poi-detail-close',
|
||||
floorId: detailReturnContext.floorId
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user