修复导览楼层切换点位错层
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user