From f13d53d5360177ecf760dac3f55c988a76f33f3d Mon Sep 17 00:00:00 2001 From: lyf <2514544224@qq.com> Date: Tue, 30 Jun 2026 20:46:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=A4=E5=86=85=E5=AF=BC?= =?UTF-8?q?=E8=A7=88=E6=A5=BC=E5=B1=82=E5=88=87=E6=8D=A2=E4=BA=8B=E5=8A=A1?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/map/ThreeMap.vue | 365 +++++++++++--------- src/components/navigation/GuideMapShell.vue | 38 +- src/data/adapters/sgsSdkGuideAdapter.ts | 11 +- 3 files changed, 249 insertions(+), 165 deletions(-) diff --git a/src/components/map/ThreeMap.vue b/src/components/map/ThreeMap.vue index 791503a..44eb380 100644 --- a/src/components/map/ThreeMap.vue +++ b/src/components/map/ThreeMap.vue @@ -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 = 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 => { + 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 = '' - } } } diff --git a/src/components/navigation/GuideMapShell.vue b/src/components/navigation/GuideMapShell.vue index e23859d..8d88019 100644 --- a/src/components/navigation/GuideMapShell.vue +++ b/src/components/navigation/GuideMapShell.vue @@ -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') } diff --git a/src/data/adapters/sgsSdkGuideAdapter.ts b/src/data/adapters/sgsSdkGuideAdapter.ts index 798a444..5db6350 100644 --- a/src/data/adapters/sgsSdkGuideAdapter.ts +++ b/src/data/adapters/sgsSdkGuideAdapter.ts @@ -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) })