|
|
|
|
@@ -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)
|
|
|
|
|
|