修复导览楼层切换点位错层

This commit is contained in:
lyf
2026-06-30 20:10:18 +08:00
parent e3682ed3ec
commit 2376923915
4 changed files with 457 additions and 35 deletions

View File

@@ -85,6 +85,7 @@ interface LoadFloorOptions {
preserveCurrentSceneUntilReady?: boolean
suppressProgress?: boolean
allowOverviewFallback?: boolean
detachPoiBeforeLoad?: boolean
}
interface LoadModelOptions {
@@ -118,6 +119,7 @@ interface PoiSpriteUserData {
hitTargetBaseScale?: number
visibleMarker?: THREE.Sprite
hitTarget?: THREE.Sprite
labelSprite?: THREE.Sprite
feedbackUntil?: number
labelBaseScaleX?: number
labelBaseScaleY?: number
@@ -303,6 +305,8 @@ const poiHitTargetScaleMultiplier = 4.8
const poiHitTargetCoreScaleMultiplier = 5.4
const poiScreenHitRadiusPx = 64
const poiScreenHitRadiusCorePx = 76
const poiAmbientLabelHallSpacingPx = 92
const poiAmbientLabelServiceSpacingPx = 72
const indoorContextLockMs = 5000
const indoorGestureExitGraceMs = 900
const overviewEntryIntentWindowMs = 1200
@@ -347,6 +351,19 @@ const poiCategoryPriority: Record<string, number> = {
operation_experience: 46
}
const isHallPoi = (poi: RenderPoi) => (
poi.kind === 'hall'
|| poi.primaryCategory === 'exhibition_hall'
|| poi.primaryCategory === 'touring_poi'
)
const isServiceFacilityPoi = (poi: RenderPoi) => (
poi.primaryCategory === 'basic_service_facility'
|| poi.primaryCategory === 'accessibility_special_service'
)
const isTransportPoi = (poi: RenderPoi) => poi.primaryCategory === 'transport_circulation'
const poiGlyphMap: Record<string, string> = {
accessible_restroom: '无',
accessibility: '无',
@@ -648,7 +665,6 @@ const getActiveModelSpan = () => {
}
const getPoiVisibilityTier = (): PoiVisibilityTier => {
if (activeView.value === 'floor') return 'full'
if (!controls || !activeModel) return 'full'
const ratio = controls.getDistance() / getActiveModelSpan()
@@ -663,12 +679,19 @@ const shouldShowPoiAtDistance = (poi: RenderPoi, tier: PoiVisibilityTier) => {
return true
}
if (activeView.value === 'floor' && isHallPoi(poi)) {
return true
}
if (tier === 'tight') {
return corePoiCategories.has(poi.primaryCategory)
return corePoiCategories.has(poi.primaryCategory) && !isTransportPoi(poi)
}
if (tier === 'balanced') {
return corePoiCategories.has(poi.primaryCategory) || poi.primaryCategory === 'touring_poi'
return !isTransportPoi(poi) && (
corePoiCategories.has(poi.primaryCategory)
|| poi.primaryCategory === 'touring_poi'
)
}
return true
@@ -689,8 +712,173 @@ const getPoiScreenSpacing = (tier: PoiVisibilityTier) => {
const getPoiPriority = (poi: RenderPoi) => {
const categoryPriority = poiCategoryPriority[poi.primaryCategory] || 20
const selectedBoost = poi.id === activeFocusPoiId.value ? 1000 : 0
const hallBoost = activeView.value === 'floor' && isHallPoi(poi) ? 40 : 0
const transportPenalty = isTransportPoi(poi) ? 18 : 0
return selectedBoost + categoryPriority
return selectedBoost + hallBoost + categoryPriority - transportPenalty
}
const getPoiLabelDensityTier = (): PoiVisibilityTier => {
if (!controls || !activeModel) return 'full'
const ratio = controls.getDistance() / getActiveModelSpan()
if (ratio >= 0.42) return 'tight'
if (ratio >= 0.26) return 'balanced'
return 'full'
}
const shouldCreateAmbientPoiLabel = (poi: RenderPoi) => (
isHallPoi(poi)
|| isServiceFacilityPoi(poi)
)
const shouldShowAmbientPoiLabel = (
poi: RenderPoi,
markerVisible: boolean,
densityTier: PoiVisibilityTier
) => {
if (!markerVisible || activeView.value !== 'floor') return false
if (poi.id === activeFocusPoiId.value || poi.primaryCategory === 'target_preview') return false
if (isHallPoi(poi)) return true
if (isTransportPoi(poi)) return false
if (isServiceFacilityPoi(poi)) return densityTier === 'full'
return false
}
const getPoiAmbientLabelPriority = (poi: RenderPoi) => {
if (isHallPoi(poi)) return getPoiPriority(poi) + 120
if (isServiceFacilityPoi(poi)) return getPoiPriority(poi) + 20
return getPoiPriority(poi)
}
const getAmbientLabelScaleBoost = (poi: RenderPoi) => {
if (!controls || !activeModel) return 1
const distanceRatio = controls.getDistance() / getActiveModelSpan()
const baseBoost = Math.min(1.55, Math.max(0.92, distanceRatio / 0.28))
return isHallPoi(poi) ? Math.max(1, baseBoost) : baseBoost
}
const updateAmbientLabelScale = (labelSprite: THREE.Sprite, poi: RenderPoi) => {
const userData = getPoiSpriteUserData(labelSprite)
const baseScaleX = typeof userData.labelBaseScaleX === 'number'
? userData.labelBaseScaleX
: labelSprite.scale.x
const baseScaleY = typeof userData.labelBaseScaleY === 'number'
? userData.labelBaseScaleY
: labelSprite.scale.y
const boost = getAmbientLabelScaleBoost(poi)
labelSprite.scale.set(baseScaleX * boost, baseScaleY * boost, 1)
}
const getProjectedScreenPosition = (object: THREE.Object3D) => {
if (!camera || !renderer) return null
const projected = object.position.clone().project(camera)
if (!Number.isFinite(projected.x) || !Number.isFinite(projected.y) || projected.z < -1 || projected.z > 1) {
return null
}
return new THREE.Vector2(
(projected.x + 1) * renderer.domElement.clientWidth * 0.5,
(1 - projected.y) * renderer.domElement.clientHeight * 0.5
)
}
const getAmbientLabelBounds = (center: THREE.Vector2, poi: RenderPoi) => {
const width = isHallPoi(poi) ? 138 : 112
const height = isHallPoi(poi) ? 44 : 34
return {
left: center.x - width * 0.5,
right: center.x + width * 0.5,
top: center.y - height * 0.5,
bottom: center.y + height * 0.5
}
}
const labelBoundsOverlap = (
left: ReturnType<typeof getAmbientLabelBounds>,
right: ReturnType<typeof getAmbientLabelBounds>,
spacing: number
) => (
left.left - spacing < right.right
&& left.right + spacing > right.left
&& left.top - spacing < right.bottom
&& left.bottom + spacing > right.top
)
const updateAmbientPoiLabels = () => {
if (!poiGroup || !camera || !renderer) return
const densityTier = getPoiLabelDensityTier()
const acceptedBounds: Array<ReturnType<typeof getAmbientLabelBounds>> = []
const markerSize = getPoiMarkerSize()
const candidates = getPoiSprites()
.map((sprite) => {
const poi = sprite.userData.poi as RenderPoi | undefined
const labelSprite = getPoiSpriteUserData(sprite).labelSprite
return poi && labelSprite
? {
marker: sprite,
labelSprite,
poi,
priority: getPoiAmbientLabelPriority(poi)
}
: null
})
.filter((candidate): candidate is {
marker: THREE.Sprite
labelSprite: THREE.Sprite
poi: RenderPoi
priority: number
} => Boolean(candidate))
.sort((left, right) => right.priority - left.priority)
candidates.forEach(({ marker, labelSprite, poi }) => {
if (!poi.positionGltf || !shouldShowAmbientPoiLabel(poi, marker.visible, densityTier)) {
labelSprite.visible = false
return
}
updateAmbientLabelScale(labelSprite, poi)
const [x, y, z] = poi.positionGltf
const baseOffset = markerSize * (isHallPoi(poi) ? 1.82 : 1.5)
const offsetAttempts = [
0,
markerSize * 0.82,
-markerSize * 0.62,
markerSize * 1.48,
-markerSize * 1.08
]
const spacing = isHallPoi(poi) ? poiAmbientLabelHallSpacingPx : poiAmbientLabelServiceSpacingPx
let accepted = false
for (const offset of offsetAttempts) {
labelSprite.position.set(x, y + baseOffset + offset, z)
const screenPosition = getProjectedScreenPosition(labelSprite)
if (!screenPosition) continue
const bounds = getAmbientLabelBounds(screenPosition, poi)
const overlaps = acceptedBounds.some((acceptedBound) => labelBoundsOverlap(bounds, acceptedBound, spacing))
if (overlaps) continue
labelSprite.visible = true
acceptedBounds.push(bounds)
accepted = true
break
}
if (!accepted) {
labelSprite.visible = false
}
})
}
const updatePoiVisibilityByDistance = () => {
@@ -725,16 +913,21 @@ const updatePoiVisibilityByDistance = () => {
}
const isSelected = poi.id === activeFocusPoiId.value || poi.primaryCategory === 'target_preview'
const isPinnedHall = activeView.value === 'floor' && isHallPoi(poi)
const categoryVisible = shouldShowPoiAtDistance(poi, tier)
const isTooClose = spacing > 0 && acceptedPositions.some((position) => position.distanceTo(screenPosition) < spacing)
const exceedsLimit = Number.isFinite(limit) && visibleCount >= limit
if (!categoryVisible || (!isSelected && (isTooClose || exceedsLimit))) {
if (!categoryVisible || (!isSelected && !isPinnedHall && (isTooClose || exceedsLimit))) {
sprite.visible = false
const hitTarget = getPoiSpriteUserData(sprite).hitTarget
const labelSprite = getPoiSpriteUserData(sprite).labelSprite
if (hitTarget) {
hitTarget.visible = false
}
if (labelSprite) {
labelSprite.visible = false
}
return
}
@@ -746,6 +939,8 @@ const updatePoiVisibilityByDistance = () => {
acceptedPositions.push(screenPosition)
visibleCount += 1
})
updateAmbientPoiLabels()
}
const refreshPoiVisibilityByDistance = () => {
@@ -903,6 +1098,8 @@ const setProgress = (progress: number, message: string) => {
}
const staleModelLoadMessage = 'STALE_MODEL_LOAD'
let isFloorSwitchLocked = false
let queuedFloorSwitchId = ''
const startModelLoad = () => {
modelLoadVersion += 1
@@ -933,6 +1130,22 @@ const assertCurrentModelLoad = (loadToken: number, staleObject?: THREE.Object3D)
throw createStaleModelLoadError()
}
const assertActiveFloorModelState = (loadToken: number, requestedFloorId: string) => {
assertCurrentModelLoad(loadToken)
const activeModelFloorId = typeof activeModel?.userData.floorId === 'string'
? activeModel.userData.floorId
: ''
if (
activeView.value !== 'floor'
|| currentFloor.value !== requestedFloorId
|| !activeModel
|| activeModelFloorId !== requestedFloorId
) {
throw new Error(`楼层模型状态不一致:请求 ${requestedFloorId},当前 ${currentFloor.value || '未知'},模型 ${activeModelFloorId || '未知'}`)
}
}
const waitForContainer = async () => {
await nextTick()
@@ -1272,6 +1485,14 @@ const detachPoiMarkerGroups = () => {
})
}
const detachActivePoiLayer = () => {
disposeFocusLabel()
disposeFocusPulse()
disposeFocusBase()
clearRoutePreview()
clearPoiGroupChildren()
}
const disposePoiMarkerCache = () => {
detachPoiMarkerGroups()
poiMarkerGroupCache.forEach((entry) => {
@@ -1783,10 +2004,39 @@ const canLoadFloorSilently = (floorId: string) => {
return Boolean(floor && canAttachCachedSharedModel(floor.modelUrl))
}
const getDefaultFloorId = () => (
floorIndex.value.find((floor) => floor.floorId === props.initialFloorId)?.floorId
|| floorIndex.value.find((floor) => floor.floorId === 'L1')?.floorId
const getFloorMatchKeys = (floor: FloorIndexItem) => (
[floor.floorId, floor.label, ...(floor.modelMatchKeys || [])]
.filter((value): value is string => Boolean(value))
)
const resolveFloorIdFromRequest = (requestedFloorId?: string | null) => {
const requested = requestedFloorId?.trim()
if (!requested) return ''
const normalizedRequest = normalizeModelMatchKey(requested)
return floorIndex.value.find((floor) => getFloorMatchKeys(floor).some((key) => (
key === requested
|| key.toLowerCase() === requested.toLowerCase()
|| normalizeModelMatchKey(key) === normalizedRequest
)))?.floorId || ''
}
const isGroundEntryFloor = (floor: FloorIndexItem) => (
getFloorMatchKeys(floor).some((key) => {
const normalizedKey = normalizeModelMatchKey(key)
return normalizedKey === 'l1' || normalizedKey === '1f'
})
)
const getPreferredInitialFloorId = () => (
floorIndex.value.find(isGroundEntryFloor)?.floorId
|| floorIndex.value[0]?.floorId
|| ''
)
const getDefaultFloorId = () => (
resolveFloorIdFromRequest(props.initialFloorId)
|| getPreferredInitialFloorId()
|| props.initialFloorId
|| 'L1'
)
@@ -2051,7 +2301,8 @@ const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
() => loadFloor(targetFloorId, {
preserveCurrentSceneUntilReady: hasSceneToKeep,
suppressProgress: hasSceneToKeep,
allowOverviewFallback: true
allowOverviewFallback: false,
detachPoiBeforeLoad: true
}),
'单楼层模型加载失败',
{ showLoading: !hasSceneToKeep && !canLoadFloorSilently(targetFloorId) }
@@ -2388,9 +2639,19 @@ const attachCachedSharedModel = (
const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
const floor = floorIndex.value.find((item) => item.floorId === floorId)
if (!floor || !scene) return
if (!floor) {
throw new Error(`未找到楼层模型:${floorId}`)
}
if (!scene) {
throw createStaleModelLoadError()
}
const loadToken = startModelLoad()
const requestedFloorId = floor.floorId
if (options.detachPoiBeforeLoad || activeView.value === 'floor' || currentFloor.value !== requestedFloorId) {
detachActivePoiLayer()
}
const canUseExactCachedModel = canAttachCachedSharedModel(floor.modelUrl)
const canUseOverviewFallback = !canUseExactCachedModel
&& Boolean(options.allowOverviewFallback)
@@ -2414,6 +2675,7 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
loadToken,
floor.floorId
)
assertActiveFloorModelState(loadToken, requestedFloorId)
} else {
let gltf: GLTF | null = null
@@ -2448,6 +2710,7 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
floor.floorId,
{ allowAnyCachedOverview: true }
)
assertActiveFloorModelState(loadToken, requestedFloorId)
}
if (gltf) {
@@ -2484,17 +2747,19 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
cachedSharedModelUrl = floor.modelUrl
}
targetScene.add(activeModel)
assertActiveFloorModelState(loadToken, requestedFloorId)
}
}
if (!activeModel) throw createStaleModelLoadError()
assertActiveFloorModelState(loadToken, requestedFloorId)
hasLoadedFloorViewOnce = true
fitCameraToObject(activeModel)
markFloorAutoSwitchEntry()
if (shouldRenderPoiMarkers.value) {
await loadFloorPOIs(floor, loadToken)
assertCurrentModelLoad(loadToken)
assertActiveFloorModelState(loadToken, requestedFloorId)
}
refreshPoiVisibilityByDistance()
@@ -2789,6 +3054,82 @@ const createPoiLabelSprite = (poi: RenderPoi, markerSize: number) => {
return sprite
}
const createAmbientPoiLabelSprite = (poi: RenderPoi, markerSize: number) => {
const canvas = document.createElement('canvas')
canvas.width = 360
canvas.height = 112
const context = canvas.getContext('2d')
const labelLimit = isHallPoi(poi) ? 10 : 7
const label = poi.name.length > labelLimit ? `${poi.name.slice(0, labelLimit)}` : poi.name
if (context) {
const color = getPoiColor(poi.primaryCategory)
const hallLabel = isHallPoi(poi)
const boxX = hallLabel ? 28 : 42
const boxY = hallLabel ? 18 : 26
const boxWidth = hallLabel ? 304 : 276
const boxHeight = hallLabel ? 60 : 48
const radius = hallLabel ? 22 : 18
context.clearRect(0, 0, canvas.width, canvas.height)
context.shadowColor = 'rgba(31, 35, 41, 0.18)'
context.shadowBlur = hallLabel ? 14 : 10
context.shadowOffsetY = hallLabel ? 7 : 5
context.fillStyle = hallLabel ? 'rgba(255, 255, 255, 0.98)' : 'rgba(255, 255, 255, 0.92)'
context.beginPath()
context.roundRect(boxX, boxY, boxWidth, boxHeight, radius)
context.fill()
context.shadowColor = 'transparent'
context.lineWidth = hallLabel ? 3 : 2
context.strokeStyle = hallLabel ? `${color}66` : 'rgba(31, 35, 41, 0.1)'
context.stroke()
context.beginPath()
context.moveTo(166, boxY + boxHeight)
context.lineTo(194, boxY + boxHeight)
context.lineTo(180, boxY + boxHeight + 18)
context.closePath()
context.fillStyle = hallLabel ? 'rgba(255, 255, 255, 0.98)' : 'rgba(255, 255, 255, 0.92)'
context.fill()
context.strokeStyle = hallLabel ? `${color}50` : 'rgba(31, 35, 41, 0.08)'
context.stroke()
context.beginPath()
context.arc(boxX + 30, boxY + boxHeight * 0.5, hallLabel ? 15 : 12, 0, Math.PI * 2)
context.fillStyle = color
context.fill()
context.font = `700 ${hallLabel ? 18 : 15}px ${CANVAS_FONT_FAMILY}`
context.textAlign = 'center'
context.textBaseline = 'middle'
context.fillStyle = '#ffffff'
context.fillText(getPoiGlyph(poi), boxX + 30, boxY + boxHeight * 0.5, 24)
context.font = `700 ${hallLabel ? 24 : 20}px ${CANVAS_FONT_FAMILY}`
context.textAlign = 'left'
context.fillStyle = '#1f2329'
context.fillText(label, boxX + 56, boxY + boxHeight * 0.5, boxWidth - 76)
}
const texture = new THREE.CanvasTexture(canvas)
texture.colorSpace = THREE.SRGBColorSpace
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthTest: false,
depthWrite: false
}))
sprite.userData.isPoiLabel = true
sprite.userData.poi = poi
sprite.userData.labelBaseScaleX = markerSize * (isHallPoi(poi) ? 3.9 : 3.05)
sprite.userData.labelBaseScaleY = markerSize * (isHallPoi(poi) ? 1.22 : 0.95)
sprite.renderOrder = isHallPoi(poi) ? 18 : 15
sprite.scale.set(sprite.userData.labelBaseScaleX, sprite.userData.labelBaseScaleY, 1)
sprite.visible = false
return sprite
}
const createPoiPulseSprite = (poi: RenderPoi, markerSize: number) => {
const canvas = document.createElement('canvas')
canvas.width = 160
@@ -3012,6 +3353,9 @@ const findLoadedPoi = (poiId: string) => (
const createPoiMarkerSprites = (poi: RenderPoi, markerSize: number) => {
const sprite = new THREE.Sprite(createPoiMaterial(poi))
const hitTarget = new THREE.Sprite(createPoiHitTargetMaterial())
const labelSprite = shouldCreateAmbientPoiLabel(poi)
? createAmbientPoiLabelSprite(poi, markerSize)
: null
const isCorePoi = corePoiCategories.has(poi.primaryCategory) || poi.primaryCategory === 'target_preview'
const hitTargetScale = markerSize * (isCorePoi ? poiHitTargetCoreScaleMultiplier : poiHitTargetScaleMultiplier)
@@ -3019,6 +3363,9 @@ const createPoiMarkerSprites = (poi: RenderPoi, markerSize: number) => {
sprite.userData.poi = poi
sprite.userData.isCorePoi = isCorePoi
sprite.userData.hitTarget = hitTarget
if (labelSprite) {
sprite.userData.labelSprite = labelSprite
}
sprite.userData.hitTargetBaseScale = hitTargetScale
hitTarget.userData.baseScale = hitTargetScale
@@ -3030,7 +3377,7 @@ const createPoiMarkerSprites = (poi: RenderPoi, markerSize: number) => {
setPoiSpriteFocusStyle(sprite, poi.id === activeFocusPoiId.value)
return { sprite, hitTarget }
return { sprite, hitTarget, labelSprite }
}
const createPoiFromFocusRequest = (request: TargetPoiFocusRequest): RenderPoi | null => {
@@ -3078,13 +3425,20 @@ const createPoiMarkerGroup = (
pois.forEach((poi) => {
const [x, y, z] = poi.positionGltf!
const { sprite, hitTarget } = createPoiMarkerSprites(poi, markerSize)
const { sprite, hitTarget, labelSprite } = createPoiMarkerSprites(poi, markerSize)
sprite.position.set(x, y + markerSize * 0.5, z)
hitTarget.position.copy(sprite.position)
if (labelSprite) {
labelSprite.position.set(x, y + markerSize * 1.82, z)
labelSprite.visible = false
}
sprite.visible = shouldShowPoiAtDistance(poi, getPoiVisibilityTier())
hitTarget.visible = sprite.visible
group.add(sprite)
group.add(hitTarget)
if (labelSprite) {
group.add(labelSprite)
}
})
return group
@@ -3104,11 +3458,17 @@ const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
if (!poiGroup) return
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
}
@@ -3116,6 +3476,9 @@ const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
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))
@@ -3133,6 +3496,9 @@ const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
disposeObject(markerGroup)
return
}
if (loadToken !== undefined) {
assertActiveFloorModelState(loadToken, floor.floorId)
}
poiMarkerGroupCache.set(markerCacheKey, entry)
attachPoiMarkerGroup(entry)
@@ -3210,7 +3576,8 @@ const handleSceneTap = async (event: PointerEvent) => {
await loadFloor(selectedPoi.floorId, {
preserveCurrentSceneUntilReady: hasRenderableSceneForFloorTransition(),
suppressProgress: true,
allowOverviewFallback: true
allowOverviewFallback: false,
detachPoiBeforeLoad: true
})
emit('floorChange', selectedPoi.floorId)
} catch (error) {
@@ -3339,7 +3706,8 @@ const focusTargetPoi = async (request: TargetPoiFocusRequest) => {
await loadFloor(request.floorId, {
preserveCurrentSceneUntilReady: hasRenderableSceneForFloorTransition(),
suppressProgress: true,
allowOverviewFallback: true
allowOverviewFallback: false,
detachPoiBeforeLoad: true
})
isLoading.value = false
emit('floorChange', request.floorId)
@@ -3407,11 +3775,11 @@ const loadModelPackage = async () => {
setProgress(14, '正在读取楼层索引...')
floorIndex.value = [...packageData.floors].sort(compareFloorsTopToBottom)
const requestedInitialFloor = floorIndex.value.find((floor) => floor.floorId === props.initialFloorId)
if (requestedInitialFloor) {
currentFloor.value = requestedInitialFloor.floorId
const requestedInitialFloorId = resolveFloorIdFromRequest(props.initialFloorId)
if (requestedInitialFloorId) {
currentFloor.value = requestedInitialFloorId
} else if (!floorIndex.value.some((floor) => floor.floorId === currentFloor.value)) {
currentFloor.value = floorIndex.value.find((floor) => floor.floorId === 'L1')?.floorId || floorIndex.value[0]?.floorId || 'L1'
currentFloor.value = getDefaultFloorId()
}
if (props.initialView === 'floor') {
@@ -3454,18 +3822,28 @@ const init3DScene = async () => {
}
const handleFloorChange = async (floorId: string) => {
const hasSceneToKeep = hasRenderableSceneForFloorTransition()
const requestedFloorId = resolveFloorIdFromRequest(floorId) || floorId
if (isFloorSwitchLocked) {
queuedFloorSwitchId = requestedFloorId
return
}
isFloorSwitchLocked = true
queuedFloorSwitchId = ''
try {
isLoading.value = true
loadError.value = false
await loadFloor(floorId, {
preserveCurrentSceneUntilReady: hasSceneToKeep,
suppressProgress: true,
allowOverviewFallback: true
setProgress(12, `正在切换到 ${formatFloorLabel(requestedFloorId)}...`)
await loadFloor(requestedFloorId, {
preserveCurrentSceneUntilReady: false,
suppressProgress: false,
allowOverviewFallback: false,
detachPoiBeforeLoad: true
})
updatePoiVisibilityByDistance()
isLoading.value = false
emit('floorChange', floorId)
emit('floorChange', requestedFloorId)
} catch (error) {
if (isStaleModelLoadError(error)) return
@@ -3473,6 +3851,15 @@ 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 = ''
}
}
}
@@ -3615,7 +4002,7 @@ const syncRequestedIndoorView = async () => {
}
if (props.initialView === 'floor') {
const requestedFloorId = floorIndex.value.find((floor) => floor.floorId === props.initialFloorId)?.floorId
const requestedFloorId = resolveFloorIdFromRequest(props.initialFloorId)
|| currentFloor.value
if (requestedFloorId && (activeView.value !== 'floor' || currentFloor.value !== requestedFloorId)) {
await handleFloorChange(requestedFloorId)

View File

@@ -151,7 +151,7 @@
:key="floor.id"
class="floor-item"
:class="{ active: activeFloorId === floor.id && layerMode !== 'multi' }"
@tap="handleFloorChange(floor.label)"
@tap.stop="handleFloorChange(floor)"
>
<text class="floor-label">{{ floor.label }}</text>
</view>
@@ -445,6 +445,7 @@ const activeFloorId = computed(() => {
return normalizedFloor?.id || normalizedFloorId
})
const activeFloorScrollIntoView = ref('')
let suppressFloorAutoScrollUntil = 0
const syncActiveFloorScroll = () => {
if (props.mapType !== 'indoor' || props.indoorView === 'overview' || props.layerMode === 'multi') {
@@ -452,6 +453,11 @@ const syncActiveFloorScroll = () => {
return
}
if (Date.now() < suppressFloorAutoScrollUntil) {
activeFloorScrollIntoView.value = ''
return
}
const matchedFloor = floorItems.value.find((floor) => floor.id === activeFloorId.value)
activeFloorScrollIntoView.value = matchedFloor?.scrollId || ''
}
@@ -517,13 +523,15 @@ const handleModeChange = (mode: '2d' | '3d') => {
emit('modeChange', mode)
}
const handleFloorChange = (floor: string) => {
const floorId = props.normalizeFloorId(floor)
const handleFloorChange = (floor: { id: string; label: string }) => {
const floorId = floor.id || props.normalizeFloorId(floor.label)
suppressFloorAutoScrollUntil = Date.now() + 650
activeFloorScrollIntoView.value = ''
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
void indoorRendererRef.value?.switchFloor?.(floorId)
emit('indoorViewChange', 'floor')
emit('layerModeChange', 'single')
emit('floorChange', floor)
emit('floorChange', floor.label)
}
const handleLayerModeChange = (mode: LayerDisplayMode) => {

View File

@@ -528,7 +528,7 @@ const loadGuideFloors = async () => {
const floors = await guideUseCase.getFloors()
guideFloors.value = floors
activeGuideFloor.value = floors.find((floor) => floor.label === activeGuideFloor.value)?.label
|| floors.find((floor) => floor.id === 'L1')?.label
|| floors.find((floor) => floor.label === '1F')?.label
|| floors[0]?.label
|| activeGuideFloor.value
} catch (error) {

View File

@@ -12,6 +12,7 @@ import {
isIndoorNavigableFloor
} from '@/domain/guideFloor'
import {
buildSgsFloorAliases,
createSgsHallPoiDiagnostics,
formatSgsFloorLabel,
toMuseumHallPoisFromSgs,
@@ -193,6 +194,16 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
const floorModelItems = floors
.map((floor, index) => {
const bundle = bundles[index]
const bundleFloor = bundle.floor
if (bundleFloor && String(bundleFloor.floorId) !== String(floor.floorId)) {
warnSgsGuideModelDiagnostics('floor bundle metadata does not match requested floor', {
requestedFloorId: String(floor.floorId),
requestedFloorCode: floor.floorCode,
bundleFloorId: String(bundleFloor.floorId),
bundleFloorCode: bundleFloor.floorCode
})
}
const modelUrl = resolveSgsAssetUrl(bundle.model?.modelUrl || bundle.model?.fallbackModelUrl)
const label = formatSgsFloorLabel(floor.floorCode, floor.floorName)
@@ -228,12 +239,28 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
async loadFloorPois(floorId: string) {
const manifest = await this.provider.getManifest()
this.floorsCache = sortSgsFloors(manifest.floors).filter(isIndoorNavigableFloor)
const aliases = buildSgsFloorAliases(this.floorsCache)
const requestedFloorId = floorId.trim()
const normalizedFloorId = aliases.get(requestedFloorId)
|| aliases.get(requestedFloorId.toLowerCase())
|| requestedFloorId
const matchedFloor = this.floorsCache.find((floor) => (
String(floor.floorId) === floorId
|| floor.floorCode === floorId
|| formatSgsFloorLabel(floor.floorCode, floor.floorName) === floorId
String(floor.floorId) === normalizedFloorId
|| floor.floorCode === normalizedFloorId
|| formatSgsFloorLabel(floor.floorCode, floor.floorName) === normalizedFloorId
))
if (!matchedFloor) return []
if (!matchedFloor) {
warnSgsGuideModelDiagnostics('unable to resolve floor for render POI request', {
requestedFloorId: floorId,
normalizedFloorId,
knownFloors: this.floorsCache.map((floor) => ({
floorId: String(floor.floorId),
floorCode: floor.floorCode,
label: formatSgsFloorLabel(floor.floorCode, floor.floorName)
}))
})
return []
}
const resolvedFloorId = String(matchedFloor.floorId)
const loadFloorData = async <T>(