修复室内导览楼层切换事务一致性

This commit is contained in:
lyf
2026-06-30 20:46:14 +08:00
parent 2376923915
commit f13d53d536
3 changed files with 249 additions and 165 deletions

View File

@@ -84,7 +84,6 @@ interface CameraFitOptions {
interface LoadFloorOptions {
preserveCurrentSceneUntilReady?: boolean
suppressProgress?: boolean
allowOverviewFallback?: boolean
detachPoiBeforeLoad?: boolean
}
@@ -179,6 +178,15 @@ interface PoiMarkerCacheEntry {
markerSize: number
}
interface PreparedFloorScene {
floor: FloorIndexItem
model: THREE.Object3D
poiEntry: PoiMarkerCacheEntry | null
ownsModel: boolean
cacheAsSharedModel: boolean
protectedResources?: ReusableModelResources
}
type ContainerRef = HTMLElement | { $el?: HTMLElement } | null
const props = withDefaults(defineProps<{
@@ -270,6 +278,7 @@ let isDisposed = false
let pendingTargetFocus: TargetPoiFocusRequest | null = null
let targetFocusQueue: Promise<unknown> = Promise.resolve()
let modelLoadVersion = 0
let pendingRequestedFloorId = ''
let hasLoadedFloorViewOnce = false
let pointerDownState: { x: number; y: number } | null = null
let cachedOverviewModel: THREE.Object3D | null = null
@@ -1098,8 +1107,6 @@ const setProgress = (progress: number, message: string) => {
}
const staleModelLoadMessage = 'STALE_MODEL_LOAD'
let isFloorSwitchLocked = false
let queuedFloorSwitchId = ''
const startModelLoad = () => {
modelLoadVersion += 1
@@ -1108,6 +1115,7 @@ const startModelLoad = () => {
const invalidateModelLoads = () => {
modelLoadVersion += 1
pendingRequestedFloorId = ''
}
const isCurrentModelLoad = (loadToken: number) => (
@@ -1146,6 +1154,60 @@ const assertActiveFloorModelState = (loadToken: number, requestedFloorId: string
}
}
const assertPreparedFloorScene = (
prepared: PreparedFloorScene,
loadToken: number,
expectedFloorId: string
) => {
assertCurrentModelLoad(loadToken, prepared.ownsModel ? prepared.model : undefined)
if (pendingRequestedFloorId !== expectedFloorId) {
if (prepared.ownsModel) {
disposeObject(prepared.model, prepared.protectedResources)
}
throw createStaleModelLoadError()
}
const modelFloorId = typeof prepared.model.userData.floorId === 'string'
? prepared.model.userData.floorId
: ''
if (modelFloorId !== expectedFloorId) {
if (prepared.ownsModel) {
disposeObject(prepared.model, prepared.protectedResources)
}
throw new Error(`楼层模型校验失败:请求 ${expectedFloorId},模型 ${modelFloorId || '未知'}`)
}
const poiFloorId = typeof prepared.poiEntry?.group.userData.floorId === 'string'
? prepared.poiEntry.group.userData.floorId
: ''
if (prepared.poiEntry && (prepared.poiEntry.floorId !== expectedFloorId || poiFloorId !== expectedFloorId)) {
if (prepared.ownsModel) {
disposeObject(prepared.model, prepared.protectedResources)
}
throw new Error(`楼层点位校验失败:请求 ${expectedFloorId},点位 ${poiFloorId || prepared.poiEntry.floorId || '未知'}`)
}
}
const assertCommittedFloorScene = (loadToken: number, expectedFloorId: string) => {
assertActiveFloorModelState(loadToken, expectedFloorId)
const poiFloorId = typeof poiGroup?.userData.floorId === 'string'
? poiGroup.userData.floorId
: ''
const poiCurrentFloor = typeof poiGroup?.userData.currentFloor === 'string'
? poiGroup.userData.currentFloor
: ''
if (
shouldRenderPoiMarkers.value
&& poiGroup
&& (poiFloorId !== expectedFloorId || poiCurrentFloor !== expectedFloorId)
) {
throw new Error(`楼层点位状态不一致:请求 ${expectedFloorId},点位 ${poiFloorId || '未知'},当前 ${poiCurrentFloor || '未知'}`)
}
}
const waitForContainer = async () => {
await nextTick()
@@ -1483,6 +1545,11 @@ const detachPoiMarkerGroups = () => {
poiMarkerGroupCache.forEach((entry) => {
entry.group.parent?.remove(entry.group)
})
if (poiGroup) {
poiGroup.userData.floorId = ''
poiGroup.userData.currentFloor = ''
}
}
const detachActivePoiLayer = () => {
@@ -1997,8 +2064,6 @@ const canAttachCachedSharedModel = (modelUrl: string) => (
Boolean(cachedOverviewModel && cachedSharedModelUrl === modelUrl && scene)
)
const canAttachCachedOverviewModel = () => Boolean(cachedOverviewModel && scene)
const canLoadFloorSilently = (floorId: string) => {
const floor = floorIndex.value.find((item) => item.floorId === floorId)
return Boolean(floor && canAttachCachedSharedModel(floor.modelUrl))
@@ -2301,7 +2366,6 @@ const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
() => loadFloor(targetFloorId, {
preserveCurrentSceneUntilReady: hasSceneToKeep,
suppressProgress: hasSceneToKeep,
allowOverviewFallback: false,
detachPoiBeforeLoad: true
}),
'单楼层模型加载失败',
@@ -2617,24 +2681,123 @@ const loadOverview = async () => {
renderRoutePreview()
}
const attachCachedSharedModel = (
modelUrl: string,
name: string,
const prepareFloorScene = async (
floor: FloorIndexItem,
loadToken: number,
floorId?: string,
options: { allowAnyCachedOverview?: boolean } = {}
options: LoadFloorOptions = {}
): Promise<PreparedFloorScene> => {
const requestedFloorId = floor.floorId
let model: THREE.Object3D | null = null
let ownsModel = true
let cacheAsSharedModel = false
let protectedResources: ReusableModelResources | undefined
const cachedExactSharedModel = canAttachCachedSharedModel(floor.modelUrl)
? cachedOverviewModel
: null
if (cachedExactSharedModel) {
model = cachedExactSharedModel
ownsModel = false
protectedResources = collectReusableModelResources(cachedExactSharedModel)
model.name = `GuideFloorModel_${requestedFloorId}`
model.userData.floorId = requestedFloorId
model.visible = true
applyModelVisibilityForView(model, 'floor', requestedFloorId)
} else {
if (!options.suppressProgress) {
setProgress(18, `正在加载 ${formatFloorLabel(requestedFloorId)} 模型...`)
}
const gltf = await loadModel(
floor.modelUrl,
`正在加载 ${formatFloorLabel(requestedFloorId)} 模型`,
loadToken,
{ suppressProgress: options.suppressProgress }
)
try {
assertCurrentModelLoad(loadToken, gltf.scene)
} catch (error) {
disposeObject(gltf.scene)
throw error
}
model = gltf.scene
model.name = `GuideFloorModel_${requestedFloorId}`
model.userData.floorId = requestedFloorId
prepareModel(model)
applyModelVisibilityForView(model, 'floor', requestedFloorId)
cacheAsSharedModel = Boolean(floor.sharedModelAsset)
if (cacheAsSharedModel) {
cacheAutoSwitchOverviewMaxDim(model)
}
}
if (!model) {
throw new Error(`楼层模型加载失败:${requestedFloorId}`)
}
let poiEntry: PoiMarkerCacheEntry | undefined | null = null
try {
poiEntry = shouldRenderPoiMarkers.value
? await prepareFloorPOIs(floor, loadToken)
: null
} catch (error) {
if (ownsModel) {
disposeObject(model, protectedResources)
}
throw error
}
const prepared: PreparedFloorScene = {
floor,
model,
poiEntry: poiEntry || null,
ownsModel,
cacheAsSharedModel,
protectedResources
}
assertPreparedFloorScene(prepared, loadToken, requestedFloorId)
return prepared
}
const commitPreparedFloorScene = (
prepared: PreparedFloorScene,
loadToken: number,
expectedFloorId: string
) => {
if (!cachedOverviewModel || (!options.allowAnyCachedOverview && cachedSharedModelUrl !== modelUrl) || !scene) return false
assertPreparedFloorScene(prepared, loadToken, expectedFloorId)
const targetScene = scene
activeModel = cachedOverviewModel
activeModel.name = name
activeModel.userData.floorId = floorId || ''
activeModel.visible = true
applyModelVisibilityForView(activeModel, floorId ? 'floor' : 'multi', floorId)
if (!targetScene) {
if (prepared.ownsModel) {
disposeObject(prepared.model, prepared.protectedResources)
}
throw createStaleModelLoadError()
}
activeView.value = 'floor'
currentFloor.value = expectedFloorId
clearSceneData()
activeModel = prepared.model
if (prepared.cacheAsSharedModel) {
cachedOverviewModel = activeModel
cachedSharedModelUrl = prepared.floor.modelUrl
}
targetScene.add(activeModel)
assertCurrentModelLoad(loadToken)
return true
if (prepared.poiEntry) {
attachPoiMarkerGroup(prepared.poiEntry)
} else if (poiGroup) {
poiGroup.userData.floorId = expectedFloorId
poiGroup.userData.currentFloor = expectedFloorId
}
assertCommittedFloorScene(loadToken, expectedFloorId)
}
const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
@@ -2648,120 +2811,20 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
const loadToken = startModelLoad()
const requestedFloorId = floor.floorId
pendingRequestedFloorId = requestedFloorId
if (options.detachPoiBeforeLoad || activeView.value === 'floor' || currentFloor.value !== requestedFloorId) {
detachActivePoiLayer()
}
const canUseExactCachedModel = canAttachCachedSharedModel(floor.modelUrl)
const canUseOverviewFallback = !canUseExactCachedModel
&& Boolean(options.allowOverviewFallback)
&& Boolean(options.preserveCurrentSceneUntilReady)
&& canAttachCachedOverviewModel()
const preserveCurrentSceneUntilReady = options.preserveCurrentSceneUntilReady && !canUseExactCachedModel
let nextModel: THREE.Object3D | null = null
if (!preserveCurrentSceneUntilReady) {
activeView.value = 'floor'
currentFloor.value = floor.floorId
clearSceneData()
}
if (canUseExactCachedModel) {
activeView.value = 'floor'
currentFloor.value = floor.floorId
attachCachedSharedModel(
floor.modelUrl,
`GuideFloorModel_${floor.floorId}`,
loadToken,
floor.floorId
)
assertActiveFloorModelState(loadToken, requestedFloorId)
} else {
let gltf: GLTF | null = null
try {
if (!options.suppressProgress) {
setProgress(18, `正在加载 ${formatFloorLabel(floor.floorId)} 模型...`)
}
gltf = await loadModel(
floor.modelUrl,
`正在加载 ${formatFloorLabel(floor.floorId)} 模型`,
loadToken,
{ suppressProgress: options.suppressProgress }
)
try {
assertCurrentModelLoad(loadToken, gltf.scene)
} catch (error) {
disposeObject(gltf.scene)
throw error
}
} catch (error) {
if (isStaleModelLoadError(error) || !canUseOverviewFallback) {
throw error
}
activeView.value = 'floor'
currentFloor.value = floor.floorId
clearSceneData()
attachCachedSharedModel(
floor.modelUrl,
`GuideFloorModel_${floor.floorId}`,
loadToken,
floor.floorId,
{ allowAnyCachedOverview: true }
)
assertActiveFloorModelState(loadToken, requestedFloorId)
}
if (gltf) {
const targetScene = scene
if (!targetScene) {
disposeObject(gltf.scene)
throw createStaleModelLoadError()
}
nextModel = gltf.scene
nextModel.name = `GuideFloorModel_${floor.floorId}`
nextModel.userData.floorId = floor.floorId
prepareModel(nextModel)
try {
assertCurrentModelLoad(loadToken, nextModel)
} catch (error) {
nextModel = null
throw error
}
if (floor.sharedModelAsset) {
cacheAutoSwitchOverviewMaxDim(nextModel)
}
applyModelVisibilityForView(nextModel, 'floor', floor.floorId)
if (preserveCurrentSceneUntilReady) {
activeView.value = 'floor'
currentFloor.value = floor.floorId
clearSceneData()
}
activeModel = nextModel
if (floor.sharedModelAsset) {
cachedOverviewModel = activeModel
cachedSharedModelUrl = floor.modelUrl
}
targetScene.add(activeModel)
assertActiveFloorModelState(loadToken, requestedFloorId)
}
}
const prepared = await prepareFloorScene(floor, loadToken, options)
commitPreparedFloorScene(prepared, loadToken, requestedFloorId)
if (!activeModel) throw createStaleModelLoadError()
assertActiveFloorModelState(loadToken, requestedFloorId)
assertCommittedFloorScene(loadToken, requestedFloorId)
hasLoadedFloorViewOnce = true
fitCameraToObject(activeModel)
markFloorAutoSwitchEntry()
if (shouldRenderPoiMarkers.value) {
await loadFloorPOIs(floor, loadToken)
assertActiveFloorModelState(loadToken, requestedFloorId)
}
refreshPoiVisibilityByDistance()
renderRoutePreview()
}
@@ -3421,6 +3484,7 @@ const createPoiMarkerGroup = (
const group = new THREE.Group()
group.name = `GuideModelPOI_${floorId}_${displayMode}`
group.userData.floorId = floorId
group.userData.currentFloor = floorId
group.userData.displayMode = displayMode
pois.forEach((poi) => {
@@ -3451,34 +3515,24 @@ const attachPoiMarkerGroup = (entry: PoiMarkerCacheEntry) => {
if (entry.group.parent !== poiGroup) {
poiGroup.add(entry.group)
}
poiGroup.userData.floorId = entry.floorId
poiGroup.userData.currentFloor = entry.floorId
updatePoiMarkerFocus()
}
const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
if (!poiGroup) return
const prepareFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) return
if (loadToken !== undefined) {
assertActiveFloorModelState(loadToken, floor.floorId)
}
const displayMode = getPoiDisplayMode()
const markerCacheKey = getPoiMarkerCacheKey(floor.floorId, displayMode)
const cachedEntry = poiMarkerGroupCache.get(markerCacheKey)
if (cachedEntry) {
if (loadToken !== undefined) {
assertActiveFloorModelState(loadToken, floor.floorId)
}
attachPoiMarkerGroup(cachedEntry)
return
return cachedEntry
}
const cachedPois = poiDataCache.get(floor.floorId)
const floorPois = cachedPois || await props.modelSource.loadFloorPois(floor.floorId)
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) return
if (loadToken !== undefined) {
assertActiveFloorModelState(loadToken, floor.floorId)
}
poiDataCache.set(floor.floorId, floorPois)
const validPois = floorPois.filter((poi) => shouldShowPoiInCurrentMode(poi))
@@ -3496,11 +3550,25 @@ const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
disposeObject(markerGroup)
return
}
poiMarkerGroupCache.set(markerCacheKey, entry)
return entry
}
const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
if (!poiGroup) return
if (loadToken !== undefined) {
assertActiveFloorModelState(loadToken, floor.floorId)
}
const entry = await prepareFloorPOIs(floor, loadToken)
if (!entry) return
if (loadToken !== undefined) {
assertActiveFloorModelState(loadToken, floor.floorId)
}
poiMarkerGroupCache.set(markerCacheKey, entry)
attachPoiMarkerGroup(entry)
}
@@ -3576,7 +3644,6 @@ const handleSceneTap = async (event: PointerEvent) => {
await loadFloor(selectedPoi.floorId, {
preserveCurrentSceneUntilReady: hasRenderableSceneForFloorTransition(),
suppressProgress: true,
allowOverviewFallback: false,
detachPoiBeforeLoad: true
})
emit('floorChange', selectedPoi.floorId)
@@ -3706,7 +3773,6 @@ const focusTargetPoi = async (request: TargetPoiFocusRequest) => {
await loadFloor(request.floorId, {
preserveCurrentSceneUntilReady: hasRenderableSceneForFloorTransition(),
suppressProgress: true,
allowOverviewFallback: false,
detachPoiBeforeLoad: true
})
isLoading.value = false
@@ -3823,22 +3889,14 @@ const init3DScene = async () => {
const handleFloorChange = async (floorId: string) => {
const requestedFloorId = resolveFloorIdFromRequest(floorId) || floorId
if (isFloorSwitchLocked) {
queuedFloorSwitchId = requestedFloorId
return
}
isFloorSwitchLocked = true
queuedFloorSwitchId = ''
try {
isLoading.value = true
loadError.value = false
setProgress(12, `正在切换到 ${formatFloorLabel(requestedFloorId)}...`)
await loadFloor(requestedFloorId, {
preserveCurrentSceneUntilReady: false,
preserveCurrentSceneUntilReady: true,
suppressProgress: false,
allowOverviewFallback: false,
detachPoiBeforeLoad: true
})
updatePoiVisibilityByDistance()
@@ -3851,15 +3909,6 @@ const handleFloorChange = async (floorId: string) => {
loadError.value = true
isLoading.value = false
setProgress(0, error instanceof Error ? error.message : '楼层模型加载失败')
} finally {
isFloorSwitchLocked = false
if (queuedFloorSwitchId && queuedFloorSwitchId !== currentFloor.value) {
const nextFloorId = queuedFloorSwitchId
queuedFloorSwitchId = ''
void handleFloorChange(nextFloorId)
} else {
queuedFloorSwitchId = ''
}
}
}

View File

@@ -445,6 +445,7 @@ const activeFloorId = computed(() => {
return normalizedFloor?.id || normalizedFloorId
})
const activeFloorScrollIntoView = ref('')
const pendingFloorId = ref('')
let suppressFloorAutoScrollUntil = 0
const syncActiveFloorScroll = () => {
@@ -523,15 +524,31 @@ const handleModeChange = (mode: '2d' | '3d') => {
emit('modeChange', mode)
}
const isStaleFloorSwitchError = (error: unknown) => (
error instanceof Error && error.message === 'STALE_MODEL_LOAD'
)
const handleFloorChange = (floor: { id: string; label: string }) => {
const floorId = floor.id || props.normalizeFloorId(floor.label)
if (!floorId || pendingFloorId.value === floorId) return
pendingFloorId.value = floorId
suppressFloorAutoScrollUntil = Date.now() + 650
activeFloorScrollIntoView.value = ''
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
void indoorRendererRef.value?.switchFloor?.(floorId)
emit('indoorViewChange', 'floor')
emit('layerModeChange', 'single')
emit('floorChange', floor.label)
Promise.resolve(indoorRendererRef.value?.switchFloor?.(floorId))
.catch((error) => {
if (isStaleFloorSwitchError(error)) return
console.error('楼层切换失败:', error)
})
.finally(() => {
if (pendingFloorId.value === floorId) {
pendingFloorId.value = ''
}
})
}
const handleLayerModeChange = (mode: LayerDisplayMode) => {
@@ -539,10 +556,22 @@ const handleLayerModeChange = (mode: LayerDisplayMode) => {
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
if (mode === 'multi') {
pendingFloorId.value = ''
void indoorRendererRef.value?.showMultiFloor?.()
emit('indoorViewChange', 'multi')
} else {
void indoorRendererRef.value?.switchFloor?.(activeFloorId.value)
const floorId = activeFloorId.value
pendingFloorId.value = floorId
Promise.resolve(indoorRendererRef.value?.switchFloor?.(floorId))
.catch((error) => {
if (isStaleFloorSwitchError(error)) return
console.error('楼层切换失败:', error)
})
.finally(() => {
if (pendingFloorId.value === floorId) {
pendingFloorId.value = ''
}
})
emit('indoorViewChange', 'floor')
}
@@ -593,8 +622,7 @@ const handleMoreTap = () => {
}
const handleThreeFloorChange = (floorId: string) => {
const floor = indoorFloors.value.find((item) => item.id === floorId)
emit('floorChange', floor?.label || floorId)
emit('floorChange', floorId)
emit('indoorViewChange', 'floor')
emit('layerModeChange', 'single')
}

View File

@@ -164,8 +164,15 @@ export const buildSgsFloorAliases = (floors: SgsSdkFloorSummaryPayload[]) => {
const label = formatSgsFloorLabel(floor.floorCode, floor.floorName)
aliases.set(floorId, floorId)
if (floor.floorCode) aliases.set(String(floor.floorCode), floorId)
if (floor.floorName) aliases.set(String(floor.floorName), floorId)
aliases.set(floorId.toLowerCase(), floorId)
if (floor.floorCode) {
aliases.set(String(floor.floorCode), floorId)
aliases.set(String(floor.floorCode).toLowerCase(), floorId)
}
if (floor.floorName) {
aliases.set(String(floor.floorName), floorId)
aliases.set(String(floor.floorName).toLowerCase(), floorId)
}
aliases.set(label, floorId)
aliases.set(label.toLowerCase(), floorId)
})