import { readonly, ref } from 'vue' export type GuideModelInitialState = Readonly<{ mode: '3d' indoorView: 'overview' | 'floor' | 'multi' layerMode: 'single' | 'multi' defaultActiveFloorId: string loadedFloorId: string selectedPoiId: null activeFocusPoiId: null targetFocusRequest: null pendingTargetFocus: null visiblePoiIds: null temporaryFocus: false poiInfoLabelVisible: false routePreviewActive: false navigationSimulationActive: false autoFloorSwitchActive: boolean }> export type PoiDetailReturnContext = Readonly<{ floorId: string resetMode: 'floor-baseline' 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 }> // 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', indoorView: 'overview', layerMode: 'single', defaultActiveFloorId: '', loadedFloorId: '', selectedPoiId: null, activeFocusPoiId: null, targetFocusRequest: null, pendingTargetFocus: null, visiblePoiIds: null, temporaryFocus: false, poiInfoLabelVisible: false, routePreviewActive: false, navigationSimulationActive: false, autoFloorSwitchActive: true }) const initialState = ref(GUIDE_MODEL_INITIAL_STATE) const pendingModelResetAfterPoiDetail = ref(null) const confirmedPoiDetailRequestId = ref(null) const requestGeneration = ref(0) const pendingViewBaselineReset = ref(null) let poiDetailRequestId = 0 export const useGuideModelState = () => { const initializeModel = ({ view, floorId }: GuideModelReadyState) => { if (initialState.value.defaultActiveFloorId) return initialState.value if (view !== 'overview' || !floorId.trim()) return initialState.value initialState.value = Object.freeze({ ...GUIDE_MODEL_INITIAL_STATE, defaultActiveFloorId: floorId, loadedFloorId: floorId }) return initialState.value } const beginPoiDetailReturn = (floorId: string) => { const context = Object.freeze({ floorId, resetMode: 'floor-baseline' as const, requestId: ++poiDetailRequestId }) pendingModelResetAfterPoiDetail.value = context confirmedPoiDetailRequestId.value = null return context } const confirmPoiDetailReturn = (requestId: number) => { if (pendingModelResetAfterPoiDetail.value?.requestId !== requestId) return false confirmedPoiDetailRequestId.value = requestId return true } const cancelPoiDetailReturn = (requestId?: number) => { if ( requestId !== undefined && pendingModelResetAfterPoiDetail.value?.requestId !== requestId ) return false pendingModelResetAfterPoiDetail.value = null confirmedPoiDetailRequestId.value = null return true } const consumePoiDetailReturn = () => { const context = pendingModelResetAfterPoiDetail.value if (!context || confirmedPoiDetailRequestId.value !== context.requestId) return null pendingModelResetAfterPoiDetail.value = null confirmedPoiDetailRequestId.value = null return context } // All async focus/floor operations compare this generation before committing. const invalidatePendingRequests = () => { requestGeneration.value += 1 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, beginViewBaselineReset, isCurrentRequest, queueViewBaselineReset, completeViewBaselineReset, consumePendingViewBaselineReset } }