169 lines
5.2 KiB
TypeScript
169 lines
5.2 KiB
TypeScript
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<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 = () => {
|
|
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
|
|
}
|
|
}
|