修复馆内导览楼层点位展示
This commit is contained in:
@@ -85,6 +85,7 @@ interface LoadFloorOptions {
|
||||
preserveCurrentSceneUntilReady?: boolean
|
||||
suppressProgress?: boolean
|
||||
detachPoiBeforeLoad?: boolean
|
||||
allowSameFloorReload?: boolean
|
||||
}
|
||||
|
||||
interface LoadModelOptions {
|
||||
@@ -176,6 +177,21 @@ interface PoiMarkerCacheEntry {
|
||||
pois: RenderPoi[]
|
||||
group: THREE.Group
|
||||
markerSize: number
|
||||
displayOffsetY: number
|
||||
}
|
||||
|
||||
interface PoiYDistribution {
|
||||
count: number
|
||||
min: number
|
||||
max: number
|
||||
median: number
|
||||
}
|
||||
|
||||
interface ModelYBounds {
|
||||
min: number
|
||||
max: number
|
||||
center: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface PreparedFloorScene {
|
||||
@@ -267,6 +283,8 @@ let routeGroup: THREE.Group | null = null
|
||||
let activeRoutePreviewSignature = ''
|
||||
const poiDataCache = new Map<string, RenderPoi[]>()
|
||||
const poiMarkerGroupCache = new Map<string, PoiMarkerCacheEntry>()
|
||||
const poiDisplayOffsetYCache = new Map<string, number>()
|
||||
const poiDisplayDiagnosticsKeys = new Set<string>()
|
||||
let activeFocusLabelSprite: THREE.Sprite | null = null
|
||||
let activeFocusPulseSprite: THREE.Sprite | null = null
|
||||
let activeFocusBaseSprite: THREE.Sprite | null = null
|
||||
@@ -279,6 +297,9 @@ let pendingTargetFocus: TargetPoiFocusRequest | null = null
|
||||
let targetFocusQueue: Promise<unknown> = Promise.resolve()
|
||||
let modelLoadVersion = 0
|
||||
let pendingRequestedFloorId = ''
|
||||
let floorSwitchLoadToken = 0
|
||||
let floorSwitchRequestedFloorId = ''
|
||||
let isFloorSwitching = false
|
||||
let hasLoadedFloorViewOnce = false
|
||||
let pointerDownState: { x: number; y: number } | null = null
|
||||
let cachedOverviewModel: THREE.Object3D | null = null
|
||||
@@ -631,6 +652,115 @@ const getPoiDisplayMode = (): PoiDisplayMode => {
|
||||
|
||||
const getPoiMarkerCacheKey = (floorId: string, displayMode = getPoiDisplayMode()) => `${floorId}:${displayMode}`
|
||||
|
||||
const summarizePoiYDistribution = (pois: RenderPoi[]): PoiYDistribution | null => {
|
||||
const values = pois
|
||||
.map((poi) => poi.positionGltf?.[1])
|
||||
.filter((value): value is number => Number.isFinite(value))
|
||||
.sort((left, right) => left - right)
|
||||
|
||||
if (!values.length) return null
|
||||
|
||||
const middle = Math.floor(values.length / 2)
|
||||
const median = values.length % 2
|
||||
? values[middle]
|
||||
: (values[middle - 1] + values[middle]) / 2
|
||||
|
||||
return {
|
||||
count: values.length,
|
||||
min: values[0],
|
||||
max: values[values.length - 1],
|
||||
median
|
||||
}
|
||||
}
|
||||
|
||||
const getModelYBounds = (model: THREE.Object3D): ModelYBounds => {
|
||||
const box = getObjectBox(model)
|
||||
const height = Math.max(box.max.y - box.min.y, 0)
|
||||
|
||||
return {
|
||||
min: box.min.y,
|
||||
max: box.max.y,
|
||||
center: (box.min.y + box.max.y) / 2,
|
||||
height
|
||||
}
|
||||
}
|
||||
|
||||
const roundDiagnosticNumber = (value: number) => Math.round(value * 1000) / 1000
|
||||
|
||||
const getPoiDisplayOffsetCacheKey = (floorId: string) => floorId
|
||||
|
||||
const getCachedPoiDisplayOffsetY = (floorId: string) => (
|
||||
poiDisplayOffsetYCache.get(getPoiDisplayOffsetCacheKey(floorId)) || 0
|
||||
)
|
||||
|
||||
const getPoiDisplayPosition = (poi: RenderPoi, floorId = poi.floorId) => {
|
||||
if (!poi.positionGltf) return null
|
||||
|
||||
const [x, y, z] = poi.positionGltf
|
||||
return new THREE.Vector3(x, y + getCachedPoiDisplayOffsetY(floorId), z)
|
||||
}
|
||||
|
||||
const resolvePoiDisplayOffsetY = (
|
||||
floorId: string,
|
||||
model: THREE.Object3D,
|
||||
pois: RenderPoi[]
|
||||
) => {
|
||||
const modelY = getModelYBounds(model)
|
||||
const poiY = summarizePoiYDistribution(pois)
|
||||
let offsetY = 0
|
||||
let reason = 'no-positioned-poi'
|
||||
|
||||
if (poiY) {
|
||||
const modelTolerance = Math.max(2, modelY.height * 0.35)
|
||||
const overlapsModelRange = poiY.max >= modelY.min - modelTolerance && poiY.min <= modelY.max + modelTolerance
|
||||
|
||||
if (overlapsModelRange) {
|
||||
reason = 'poi-y-overlaps-model-y'
|
||||
} else {
|
||||
offsetY = modelY.center - poiY.median
|
||||
reason = 'align-poi-median-to-model-center'
|
||||
}
|
||||
}
|
||||
|
||||
poiDisplayOffsetYCache.set(getPoiDisplayOffsetCacheKey(floorId), offsetY)
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const diagnosticsKey = [
|
||||
floorId,
|
||||
roundDiagnosticNumber(modelY.min),
|
||||
roundDiagnosticNumber(modelY.max),
|
||||
poiY ? roundDiagnosticNumber(poiY.min) : 'none',
|
||||
poiY ? roundDiagnosticNumber(poiY.max) : 'none',
|
||||
roundDiagnosticNumber(offsetY)
|
||||
].join(':')
|
||||
|
||||
if (!poiDisplayDiagnosticsKeys.has(diagnosticsKey)) {
|
||||
poiDisplayDiagnosticsKeys.add(diagnosticsKey)
|
||||
console.info('[ThreeMap] floor POI display coordinate diagnostics', {
|
||||
floorId,
|
||||
modelY: {
|
||||
min: roundDiagnosticNumber(modelY.min),
|
||||
max: roundDiagnosticNumber(modelY.max),
|
||||
center: roundDiagnosticNumber(modelY.center),
|
||||
height: roundDiagnosticNumber(modelY.height)
|
||||
},
|
||||
poiY: poiY
|
||||
? {
|
||||
count: poiY.count,
|
||||
min: roundDiagnosticNumber(poiY.min),
|
||||
max: roundDiagnosticNumber(poiY.max),
|
||||
median: roundDiagnosticNumber(poiY.median)
|
||||
}
|
||||
: null,
|
||||
displayOffsetY: roundDiagnosticNumber(offsetY),
|
||||
reason
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return offsetY
|
||||
}
|
||||
|
||||
const getPoiGlyph = (poi: RenderPoi) => (
|
||||
poiGlyphMap[poi.iconType] || poi.name.charAt(0) || '点'
|
||||
)
|
||||
@@ -850,14 +980,14 @@ const updateAmbientPoiLabels = () => {
|
||||
.sort((left, right) => right.priority - left.priority)
|
||||
|
||||
candidates.forEach(({ marker, labelSprite, poi }) => {
|
||||
if (!poi.positionGltf || !shouldShowAmbientPoiLabel(poi, marker.visible, densityTier)) {
|
||||
const displayPosition = getPoiDisplayPosition(poi)
|
||||
if (!displayPosition || !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,
|
||||
@@ -870,7 +1000,7 @@ const updateAmbientPoiLabels = () => {
|
||||
let accepted = false
|
||||
|
||||
for (const offset of offsetAttempts) {
|
||||
labelSprite.position.set(x, y + baseOffset + offset, z)
|
||||
labelSprite.position.set(displayPosition.x, displayPosition.y + baseOffset + offset, displayPosition.z)
|
||||
const screenPosition = getProjectedScreenPosition(labelSprite)
|
||||
if (!screenPosition) continue
|
||||
|
||||
@@ -885,7 +1015,17 @@ const updateAmbientPoiLabels = () => {
|
||||
}
|
||||
|
||||
if (!accepted) {
|
||||
labelSprite.visible = false
|
||||
if (isHallPoi(poi)) {
|
||||
const fallbackOffset = offsetAttempts[offsetAttempts.length - 1]
|
||||
labelSprite.position.set(
|
||||
displayPosition.x,
|
||||
displayPosition.y + baseOffset + fallbackOffset,
|
||||
displayPosition.z
|
||||
)
|
||||
labelSprite.visible = true
|
||||
} else {
|
||||
labelSprite.visible = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1116,6 +1256,9 @@ const startModelLoad = () => {
|
||||
const invalidateModelLoads = () => {
|
||||
modelLoadVersion += 1
|
||||
pendingRequestedFloorId = ''
|
||||
floorSwitchLoadToken = 0
|
||||
floorSwitchRequestedFloorId = ''
|
||||
isFloorSwitching = false
|
||||
}
|
||||
|
||||
const isCurrentModelLoad = (loadToken: number) => (
|
||||
@@ -1128,6 +1271,46 @@ const isStaleModelLoadError = (error: unknown) => (
|
||||
error instanceof Error && error.message === staleModelLoadMessage
|
||||
)
|
||||
|
||||
const startFloorContextTransaction = (requestedFloorId: string) => {
|
||||
const loadToken = startModelLoad()
|
||||
pendingRequestedFloorId = requestedFloorId
|
||||
floorSwitchLoadToken = loadToken
|
||||
floorSwitchRequestedFloorId = requestedFloorId
|
||||
isFloorSwitching = true
|
||||
return loadToken
|
||||
}
|
||||
|
||||
const isCurrentFloorContextTransaction = (loadToken: number, requestedFloorId: string) => (
|
||||
isFloorSwitching
|
||||
&& floorSwitchLoadToken === loadToken
|
||||
&& floorSwitchRequestedFloorId === requestedFloorId
|
||||
&& pendingRequestedFloorId === requestedFloorId
|
||||
&& isCurrentModelLoad(loadToken)
|
||||
)
|
||||
|
||||
const assertCurrentFloorContextTransaction = (
|
||||
loadToken: number,
|
||||
requestedFloorId: string,
|
||||
staleObject?: THREE.Object3D
|
||||
) => {
|
||||
if (isCurrentFloorContextTransaction(loadToken, requestedFloorId)) return
|
||||
|
||||
if (staleObject) {
|
||||
disposeObject(staleObject)
|
||||
}
|
||||
|
||||
throw createStaleModelLoadError()
|
||||
}
|
||||
|
||||
const completeFloorContextTransaction = (loadToken: number, requestedFloorId: string) => {
|
||||
if (!isCurrentFloorContextTransaction(loadToken, requestedFloorId)) return
|
||||
|
||||
isFloorSwitching = false
|
||||
floorSwitchLoadToken = 0
|
||||
floorSwitchRequestedFloorId = ''
|
||||
pendingRequestedFloorId = ''
|
||||
}
|
||||
|
||||
const assertCurrentModelLoad = (loadToken: number, staleObject?: THREE.Object3D) => {
|
||||
if (isCurrentModelLoad(loadToken)) return
|
||||
|
||||
@@ -1556,8 +1739,9 @@ const detachActivePoiLayer = () => {
|
||||
disposeFocusLabel()
|
||||
disposeFocusPulse()
|
||||
disposeFocusBase()
|
||||
clearFocusHallHighlight()
|
||||
clearRoutePreview()
|
||||
clearPoiGroupChildren()
|
||||
detachPoiMarkerGroups()
|
||||
}
|
||||
|
||||
const disposePoiMarkerCache = () => {
|
||||
@@ -1567,6 +1751,8 @@ const disposePoiMarkerCache = () => {
|
||||
})
|
||||
poiMarkerGroupCache.clear()
|
||||
poiDataCache.clear()
|
||||
poiDisplayOffsetYCache.clear()
|
||||
poiDisplayDiagnosticsKeys.clear()
|
||||
}
|
||||
|
||||
const clearPoiGroupChildren = () => {
|
||||
@@ -1679,7 +1865,8 @@ const applyFocusHallMaterial = (material: THREE.Material, seenMaterials: Set<THR
|
||||
}
|
||||
|
||||
const createFocusHallGlowMesh = (poi: RenderPoi) => {
|
||||
if (!poi.positionGltf) return null
|
||||
const displayPosition = getPoiDisplayPosition(poi)
|
||||
if (!displayPosition) return null
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 256
|
||||
@@ -1711,10 +1898,9 @@ const createFocusHallGlowMesh = (poi: RenderPoi) => {
|
||||
const hallGlowSize = Math.max(getPoiMarkerSize() * 14, getActiveModelSpan() * 0.14)
|
||||
const geometry = new THREE.PlaneGeometry(hallGlowSize, hallGlowSize * 0.72)
|
||||
const mesh = new THREE.Mesh(geometry, material)
|
||||
const [x, y, z] = poi.positionGltf
|
||||
|
||||
mesh.name = 'GuideFocusHallGlow'
|
||||
mesh.position.set(x, y + 0.08, z)
|
||||
mesh.position.set(displayPosition.x, displayPosition.y + 0.08, displayPosition.z)
|
||||
mesh.rotation.x = -Math.PI / 2
|
||||
mesh.renderOrder = 7
|
||||
|
||||
@@ -2363,11 +2549,13 @@ const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
|
||||
trigger: 'zoom-in',
|
||||
distance
|
||||
},
|
||||
() => loadFloor(targetFloorId, {
|
||||
preserveCurrentSceneUntilReady: hasSceneToKeep,
|
||||
suppressProgress: hasSceneToKeep,
|
||||
detachPoiBeforeLoad: true
|
||||
}),
|
||||
async () => {
|
||||
await loadFloor(targetFloorId, {
|
||||
preserveCurrentSceneUntilReady: hasSceneToKeep,
|
||||
suppressProgress: hasSceneToKeep,
|
||||
detachPoiBeforeLoad: true
|
||||
})
|
||||
},
|
||||
'单楼层模型加载失败',
|
||||
{ showLoading: !hasSceneToKeep && !canLoadFloorSilently(targetFloorId) }
|
||||
)
|
||||
@@ -2436,7 +2624,7 @@ const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
|
||||
|
||||
const runAutoSwitchLoad = async (
|
||||
event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: 'zoom-in' | 'zoom-out'; distance: number },
|
||||
loadTask: () => Promise<void>,
|
||||
loadTask: () => Promise<unknown>,
|
||||
fallbackMessage: string,
|
||||
options: { showLoading?: boolean } = {}
|
||||
) => {
|
||||
@@ -2611,7 +2799,16 @@ const loadCurrentFloorPoiMarkers = async (loadToken: number) => {
|
||||
const floor = floorIndex.value.find((item) => item.floorId === currentFloor.value) || floorIndex.value[0]
|
||||
if (!floor) return
|
||||
|
||||
await loadFloorPOIs(floor, loadToken)
|
||||
if (activeView.value === 'floor') {
|
||||
await loadFloorPOIs(floor, loadToken)
|
||||
assertCurrentModelLoad(loadToken)
|
||||
return
|
||||
}
|
||||
|
||||
const entry = await prepareFloorPOIs(floor, loadToken)
|
||||
if (entry) {
|
||||
attachPoiMarkerGroup(entry)
|
||||
}
|
||||
assertCurrentModelLoad(loadToken)
|
||||
}
|
||||
|
||||
@@ -2742,7 +2939,7 @@ const prepareFloorScene = async (
|
||||
let poiEntry: PoiMarkerCacheEntry | undefined | null = null
|
||||
try {
|
||||
poiEntry = shouldRenderPoiMarkers.value
|
||||
? await prepareFloorPOIs(floor, loadToken)
|
||||
? await prepareFloorPOIs(floor, loadToken, model)
|
||||
: null
|
||||
} catch (error) {
|
||||
if (ownsModel) {
|
||||
@@ -2769,6 +2966,7 @@ const commitPreparedFloorScene = (
|
||||
loadToken: number,
|
||||
expectedFloorId: string
|
||||
) => {
|
||||
assertCurrentFloorContextTransaction(loadToken, expectedFloorId, prepared.ownsModel ? prepared.model : undefined)
|
||||
assertPreparedFloorScene(prepared, loadToken, expectedFloorId)
|
||||
|
||||
const targetScene = scene
|
||||
@@ -2783,6 +2981,7 @@ const commitPreparedFloorScene = (
|
||||
currentFloor.value = expectedFloorId
|
||||
clearSceneData()
|
||||
activeModel = prepared.model
|
||||
activeModel.userData.floorId = expectedFloorId
|
||||
|
||||
if (prepared.cacheAsSharedModel) {
|
||||
cachedOverviewModel = activeModel
|
||||
@@ -2798,6 +2997,15 @@ const commitPreparedFloorScene = (
|
||||
}
|
||||
|
||||
assertCommittedFloorScene(loadToken, expectedFloorId)
|
||||
|
||||
activeFocusPoiId.value = ''
|
||||
selectedPOI.value = null
|
||||
updatePoiMarkerFocus()
|
||||
hasLoadedFloorViewOnce = true
|
||||
fitCameraToObject(activeModel)
|
||||
markFloorAutoSwitchEntry()
|
||||
refreshPoiVisibilityByDistance()
|
||||
renderRoutePreview()
|
||||
}
|
||||
|
||||
const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
|
||||
@@ -2809,24 +3017,35 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
|
||||
throw createStaleModelLoadError()
|
||||
}
|
||||
|
||||
const loadToken = startModelLoad()
|
||||
const requestedFloorId = floor.floorId
|
||||
pendingRequestedFloorId = requestedFloorId
|
||||
if (options.detachPoiBeforeLoad || activeView.value === 'floor' || currentFloor.value !== requestedFloorId) {
|
||||
if (
|
||||
!options.allowSameFloorReload
|
||||
&& activeView.value === 'floor'
|
||||
&& currentFloor.value === requestedFloorId
|
||||
&& activeModel?.userData.floorId === requestedFloorId
|
||||
&& !isFloorSwitching
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const loadToken = startFloorContextTransaction(requestedFloorId)
|
||||
if (options.detachPoiBeforeLoad) {
|
||||
detachActivePoiLayer()
|
||||
}
|
||||
|
||||
const prepared = await prepareFloorScene(floor, loadToken, options)
|
||||
commitPreparedFloorScene(prepared, loadToken, requestedFloorId)
|
||||
|
||||
if (!activeModel) throw createStaleModelLoadError()
|
||||
assertCommittedFloorScene(loadToken, requestedFloorId)
|
||||
hasLoadedFloorViewOnce = true
|
||||
fitCameraToObject(activeModel)
|
||||
markFloorAutoSwitchEntry()
|
||||
|
||||
refreshPoiVisibilityByDistance()
|
||||
renderRoutePreview()
|
||||
try {
|
||||
const prepared = await prepareFloorScene(floor, loadToken, options)
|
||||
assertCurrentFloorContextTransaction(loadToken, requestedFloorId, prepared.ownsModel ? prepared.model : undefined)
|
||||
commitPreparedFloorScene(prepared, loadToken, requestedFloorId)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (!isStaleModelLoadError(error)) {
|
||||
restoreCommittedFloorPoiLayer()
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
completeFloorContextTransaction(loadToken, requestedFloorId)
|
||||
}
|
||||
}
|
||||
|
||||
const loadMultiFloor = async () => {
|
||||
@@ -3290,39 +3509,39 @@ const disposeFocusBase = () => {
|
||||
}
|
||||
|
||||
const showFocusPoiLabel = (poi: RenderPoi) => {
|
||||
if (!poiGroup || !poi.positionGltf) return
|
||||
const displayPosition = getPoiDisplayPosition(poi)
|
||||
if (!poiGroup || !displayPosition) return
|
||||
|
||||
disposeFocusLabel()
|
||||
|
||||
const markerSize = getPoiMarkerSize()
|
||||
const [x, y, z] = poi.positionGltf
|
||||
activeFocusLabelSprite = createPoiLabelSprite(poi, markerSize)
|
||||
activeFocusLabelSprite.position.set(x, y + markerSize * 2.35, z)
|
||||
activeFocusLabelSprite.position.set(displayPosition.x, displayPosition.y + markerSize * 2.35, displayPosition.z)
|
||||
updateFocusLabelScale()
|
||||
poiGroup.add(activeFocusLabelSprite)
|
||||
}
|
||||
|
||||
const showFocusPoiBase = (poi: RenderPoi) => {
|
||||
if (!poiGroup || !poi.positionGltf) return
|
||||
const displayPosition = getPoiDisplayPosition(poi)
|
||||
if (!poiGroup || !displayPosition) return
|
||||
|
||||
disposeFocusBase()
|
||||
|
||||
const markerSize = getPoiMarkerSize()
|
||||
const [x, y, z] = poi.positionGltf
|
||||
activeFocusBaseSprite = createPoiBaseSprite(poi, markerSize)
|
||||
activeFocusBaseSprite.position.set(x, y + markerSize * 0.04, z)
|
||||
activeFocusBaseSprite.position.set(displayPosition.x, displayPosition.y + markerSize * 0.04, displayPosition.z)
|
||||
poiGroup.add(activeFocusBaseSprite)
|
||||
}
|
||||
|
||||
const showFocusPoiPulse = (poi: RenderPoi) => {
|
||||
if (!poiGroup || !poi.positionGltf) return
|
||||
const displayPosition = getPoiDisplayPosition(poi)
|
||||
if (!poiGroup || !displayPosition) return
|
||||
|
||||
disposeFocusPulse()
|
||||
|
||||
const markerSize = getPoiMarkerSize()
|
||||
const [x, y, z] = poi.positionGltf
|
||||
activeFocusPulseSprite = createPoiPulseSprite(poi, markerSize)
|
||||
activeFocusPulseSprite.position.set(x, y + markerSize * 0.15, z)
|
||||
activeFocusPulseSprite.position.set(displayPosition.x, displayPosition.y + markerSize * 0.15, displayPosition.z)
|
||||
poiGroup.add(activeFocusPulseSprite)
|
||||
}
|
||||
|
||||
@@ -3462,12 +3681,12 @@ const addFocusMarkerFromRequest = (request: TargetPoiFocusRequest) => {
|
||||
if (!poiGroup || findPoiSprite(request.poiId)) return findLoadedPoi(request.poiId) || null
|
||||
|
||||
const poi = createPoiFromFocusRequest(request)
|
||||
if (!poi?.positionGltf) return null
|
||||
const displayPosition = poi ? getPoiDisplayPosition(poi, request.floorId) : null
|
||||
if (!poi || !displayPosition) return null
|
||||
|
||||
const [x, y, z] = poi.positionGltf
|
||||
const markerSize = getPoiMarkerSize()
|
||||
const { sprite, hitTarget } = createPoiMarkerSprites(poi, markerSize)
|
||||
sprite.position.set(x, y + markerSize * 0.5, z)
|
||||
sprite.position.set(displayPosition.x, displayPosition.y + markerSize * 0.5, displayPosition.z)
|
||||
hitTarget.position.copy(sprite.position)
|
||||
poiGroup.add(sprite)
|
||||
poiGroup.add(hitTarget)
|
||||
@@ -3488,12 +3707,14 @@ const createPoiMarkerGroup = (
|
||||
group.userData.displayMode = displayMode
|
||||
|
||||
pois.forEach((poi) => {
|
||||
const [x, y, z] = poi.positionGltf!
|
||||
const displayPosition = getPoiDisplayPosition(poi, floorId)
|
||||
if (!displayPosition) return
|
||||
|
||||
const { sprite, hitTarget, labelSprite } = createPoiMarkerSprites(poi, markerSize)
|
||||
sprite.position.set(x, y + markerSize * 0.5, z)
|
||||
sprite.position.set(displayPosition.x, displayPosition.y + markerSize * 0.5, displayPosition.z)
|
||||
hitTarget.position.copy(sprite.position)
|
||||
if (labelSprite) {
|
||||
labelSprite.position.set(x, y + markerSize * 1.82, z)
|
||||
labelSprite.position.set(displayPosition.x, displayPosition.y + markerSize * 1.82, displayPosition.z)
|
||||
labelSprite.visible = false
|
||||
}
|
||||
sprite.visible = shouldShowPoiAtDistance(poi, getPoiVisibilityTier())
|
||||
@@ -3520,7 +3741,39 @@ const attachPoiMarkerGroup = (entry: PoiMarkerCacheEntry) => {
|
||||
updatePoiMarkerFocus()
|
||||
}
|
||||
|
||||
const prepareFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
|
||||
const restoreCommittedFloorPoiLayer = () => {
|
||||
if (!poiGroup || activeView.value !== 'floor' || !activeModel) return
|
||||
|
||||
const floorId = currentFloor.value
|
||||
const modelFloorId = typeof activeModel.userData.floorId === 'string'
|
||||
? activeModel.userData.floorId
|
||||
: ''
|
||||
if (!floorId || modelFloorId !== floorId) return
|
||||
|
||||
const entry = shouldRenderPoiMarkers.value
|
||||
? poiMarkerGroupCache.get(getPoiMarkerCacheKey(floorId))
|
||||
: null
|
||||
if (entry?.floorId === floorId) {
|
||||
attachPoiMarkerGroup(entry)
|
||||
refreshPoiVisibilityByDistance()
|
||||
return
|
||||
}
|
||||
|
||||
poiGroup.userData.floorId = floorId
|
||||
poiGroup.userData.currentFloor = floorId
|
||||
}
|
||||
|
||||
const getPoiMarkerSizeForModel = (model: THREE.Object3D | null) => (
|
||||
model
|
||||
? Math.max(new THREE.Box3().setFromObject(model).getSize(new THREE.Vector3()).length() * 0.012, 2.4)
|
||||
: getPoiMarkerSize()
|
||||
)
|
||||
|
||||
const prepareFloorPOIs = async (
|
||||
floor: FloorIndexItem,
|
||||
loadToken?: number,
|
||||
markerModel: THREE.Object3D | null = activeModel
|
||||
) => {
|
||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) return
|
||||
|
||||
const displayMode = getPoiDisplayMode()
|
||||
@@ -3536,14 +3789,18 @@ const prepareFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
|
||||
|
||||
poiDataCache.set(floor.floorId, floorPois)
|
||||
const validPois = floorPois.filter((poi) => shouldShowPoiInCurrentMode(poi))
|
||||
const markerSize = getPoiMarkerSize()
|
||||
const displayOffsetY = markerModel
|
||||
? resolvePoiDisplayOffsetY(floor.floorId, markerModel, validPois)
|
||||
: getCachedPoiDisplayOffsetY(floor.floorId)
|
||||
const markerSize = getPoiMarkerSizeForModel(markerModel)
|
||||
const markerGroup = createPoiMarkerGroup(floor.floorId, displayMode, validPois, markerSize)
|
||||
const entry: PoiMarkerCacheEntry = {
|
||||
floorId: floor.floorId,
|
||||
displayMode,
|
||||
pois: validPois,
|
||||
group: markerGroup,
|
||||
markerSize
|
||||
markerSize,
|
||||
displayOffsetY
|
||||
}
|
||||
|
||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
||||
@@ -3727,10 +3984,10 @@ const isSceneReadyForTargetFocus = () => (
|
||||
)
|
||||
|
||||
const focusCameraOnPoi = (poi: RenderPoi) => {
|
||||
if (!camera || !controls || !poi.positionGltf) return
|
||||
const displayPosition = getPoiDisplayPosition(poi)
|
||||
if (!camera || !controls || !displayPosition) return
|
||||
|
||||
const [x, y, z] = poi.positionGltf
|
||||
const target = new THREE.Vector3(x, y, z)
|
||||
const target = displayPosition.clone()
|
||||
const modelSize = activeModel
|
||||
? new THREE.Box3().setFromObject(activeModel).getSize(new THREE.Vector3())
|
||||
: new THREE.Vector3(80, 80, 80)
|
||||
@@ -3889,26 +4146,46 @@ const init3DScene = async () => {
|
||||
|
||||
const handleFloorChange = async (floorId: string) => {
|
||||
const requestedFloorId = resolveFloorIdFromRequest(floorId) || floorId
|
||||
const isCommittedSameFloor = (
|
||||
activeView.value === 'floor'
|
||||
&& currentFloor.value === requestedFloorId
|
||||
&& activeModel?.userData.floorId === requestedFloorId
|
||||
&& !isFloorSwitching
|
||||
)
|
||||
if (isCommittedSameFloor) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isFloorSwitching && floorSwitchRequestedFloorId === requestedFloorId) {
|
||||
return
|
||||
}
|
||||
|
||||
const previousLoadToken = modelLoadVersion
|
||||
try {
|
||||
isLoading.value = true
|
||||
loadError.value = false
|
||||
setProgress(12, `正在切换到 ${formatFloorLabel(requestedFloorId)}...`)
|
||||
await loadFloor(requestedFloorId, {
|
||||
const didCommit = await loadFloor(requestedFloorId, {
|
||||
preserveCurrentSceneUntilReady: true,
|
||||
suppressProgress: false,
|
||||
detachPoiBeforeLoad: true
|
||||
})
|
||||
updatePoiVisibilityByDistance()
|
||||
isLoading.value = false
|
||||
emit('floorChange', requestedFloorId)
|
||||
if (didCommit) {
|
||||
emit('floorChange', requestedFloorId)
|
||||
}
|
||||
} catch (error) {
|
||||
if (isStaleModelLoadError(error)) return
|
||||
|
||||
console.error('楼层模型加载失败:', error)
|
||||
loadError.value = true
|
||||
isLoading.value = false
|
||||
restoreCommittedFloorPoiLayer()
|
||||
setProgress(0, error instanceof Error ? error.message : '楼层模型加载失败')
|
||||
} finally {
|
||||
const requestWasSuperseded = modelLoadVersion > previousLoadToken + 1
|
||||
if (!requestWasSuperseded || floorSwitchRequestedFloorId === requestedFloorId) {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,12 @@
|
||||
:id="floor.scrollId"
|
||||
:key="floor.id"
|
||||
class="floor-item"
|
||||
:class="{ active: activeFloorId === floor.id && layerMode !== 'multi' }"
|
||||
:class="{
|
||||
active: activeFloorId === floor.id && layerMode !== 'multi',
|
||||
pending: loadingFloorId === floor.id,
|
||||
failed: failedFloorId === floor.id,
|
||||
disabled: isFloorSwitchDisabled(floor.id)
|
||||
}"
|
||||
@tap.stop="handleFloorChange(floor)"
|
||||
>
|
||||
<text class="floor-label">{{ floor.label }}</text>
|
||||
@@ -247,6 +252,11 @@ interface GuideFloorOption {
|
||||
label: string
|
||||
}
|
||||
|
||||
interface FloorSwitchEvent {
|
||||
floorId: string
|
||||
floorLabel: string
|
||||
}
|
||||
|
||||
interface OutdoorNavPolyline {
|
||||
points: Array<{ latitude: number; longitude: number }>
|
||||
color: string
|
||||
@@ -381,7 +391,9 @@ const props = withDefaults(defineProps<{
|
||||
const emit = defineEmits<{
|
||||
searchTap: []
|
||||
modeChange: [mode: '2d' | '3d']
|
||||
floorRequest: [event: FloorSwitchEvent]
|
||||
floorChange: [floor: string]
|
||||
floorSwitchFailed: [event: FloorSwitchEvent]
|
||||
toolClick: [tool: string]
|
||||
moreClick: []
|
||||
indoorViewChange: [view: IndoorViewMode]
|
||||
@@ -445,8 +457,15 @@ const activeFloorId = computed(() => {
|
||||
return normalizedFloor?.id || normalizedFloorId
|
||||
})
|
||||
const activeFloorScrollIntoView = ref('')
|
||||
const pendingFloorId = ref('')
|
||||
let suppressFloorAutoScrollUntil = 0
|
||||
const requestedFloorId = ref('')
|
||||
const requestedFloorLabel = ref('')
|
||||
const loadingFloorId = ref('')
|
||||
const renderedFloorId = ref(activeFloorId.value)
|
||||
const activeFloorSwitchRequestSeq = ref(0)
|
||||
const renderedFloorSwitchRequestSeq = ref(0)
|
||||
const failedFloorId = ref('')
|
||||
let suppressFloorAutoScrollForFloorId = ''
|
||||
let floorSwitchRequestSeq = 0
|
||||
|
||||
const syncActiveFloorScroll = () => {
|
||||
if (props.mapType !== 'indoor' || props.indoorView === 'overview' || props.layerMode === 'multi') {
|
||||
@@ -454,10 +473,12 @@ const syncActiveFloorScroll = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (Date.now() < suppressFloorAutoScrollUntil) {
|
||||
if (activeFloorId.value === suppressFloorAutoScrollForFloorId) {
|
||||
activeFloorScrollIntoView.value = ''
|
||||
suppressFloorAutoScrollForFloorId = ''
|
||||
return
|
||||
}
|
||||
suppressFloorAutoScrollForFloorId = ''
|
||||
|
||||
const matchedFloor = floorItems.value.find((floor) => floor.id === activeFloorId.value)
|
||||
activeFloorScrollIntoView.value = matchedFloor?.scrollId || ''
|
||||
@@ -475,6 +496,11 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(activeFloorId, (floorId) => {
|
||||
if (!floorId || loadingFloorId.value === floorId) return
|
||||
renderedFloorId.value = floorId
|
||||
})
|
||||
|
||||
const searchFieldStyle = computed(() => ({
|
||||
top: props.searchTop
|
||||
}))
|
||||
@@ -528,27 +554,77 @@ 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
|
||||
const findFloorItemById = (floorId: string) => (
|
||||
floorItems.value.find((floor) => floor.id === floorId)
|
||||
)
|
||||
|
||||
pendingFloorId.value = floorId
|
||||
suppressFloorAutoScrollUntil = Date.now() + 650
|
||||
const isFloorSwitchDisabled = (floorId: string) => (
|
||||
Boolean(loadingFloorId.value) && loadingFloorId.value !== floorId
|
||||
)
|
||||
|
||||
const clearFloorLoadingIfCurrent = (floorId: string) => {
|
||||
if (loadingFloorId.value === floorId) {
|
||||
loadingFloorId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const markFloorSwitchFailedIfUnrendered = (floorId: string, requestSeq: number) => {
|
||||
if (
|
||||
loadingFloorId.value !== floorId
|
||||
|| activeFloorSwitchRequestSeq.value !== requestSeq
|
||||
|| renderedFloorSwitchRequestSeq.value === requestSeq
|
||||
) return
|
||||
|
||||
failedFloorId.value = floorId
|
||||
clearFloorLoadingIfCurrent(floorId)
|
||||
emit('floorSwitchFailed', {
|
||||
floorId,
|
||||
floorLabel: requestedFloorId.value === floorId
|
||||
? requestedFloorLabel.value || findFloorItemById(floorId)?.label || floorId
|
||||
: findFloorItemById(floorId)?.label || floorId
|
||||
})
|
||||
}
|
||||
|
||||
const handleFloorChange = (floor: { id: string; label: string }) => {
|
||||
const floorId = floor.id
|
||||
if (!floorId || loadingFloorId.value) return
|
||||
if (floorId === renderedFloorId.value && activeFloorId.value === floorId && props.layerMode !== 'multi') {
|
||||
emit('floorChange', floorId)
|
||||
emit('indoorViewChange', 'floor')
|
||||
emit('layerModeChange', 'single')
|
||||
return
|
||||
}
|
||||
|
||||
requestedFloorId.value = floorId
|
||||
requestedFloorLabel.value = floor.label
|
||||
loadingFloorId.value = floorId
|
||||
const requestSeq = ++floorSwitchRequestSeq
|
||||
activeFloorSwitchRequestSeq.value = requestSeq
|
||||
failedFloorId.value = ''
|
||||
suppressFloorAutoScrollForFloorId = floorId
|
||||
activeFloorScrollIntoView.value = ''
|
||||
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
|
||||
emit('floorRequest', {
|
||||
floorId,
|
||||
floorLabel: floor.label
|
||||
})
|
||||
emit('indoorViewChange', 'floor')
|
||||
emit('layerModeChange', 'single')
|
||||
|
||||
Promise.resolve(indoorRendererRef.value?.switchFloor?.(floorId))
|
||||
.then(() => {
|
||||
markFloorSwitchFailedIfUnrendered(floorId, requestSeq)
|
||||
})
|
||||
.catch((error) => {
|
||||
if (isStaleFloorSwitchError(error)) return
|
||||
failedFloorId.value = floorId
|
||||
clearFloorLoadingIfCurrent(floorId)
|
||||
emit('floorSwitchFailed', {
|
||||
floorId,
|
||||
floorLabel: floor.label
|
||||
})
|
||||
console.error('楼层切换失败:', error)
|
||||
})
|
||||
.finally(() => {
|
||||
if (pendingFloorId.value === floorId) {
|
||||
pendingFloorId.value = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleLayerModeChange = (mode: LayerDisplayMode) => {
|
||||
@@ -556,22 +632,45 @@ const handleLayerModeChange = (mode: LayerDisplayMode) => {
|
||||
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
|
||||
|
||||
if (mode === 'multi') {
|
||||
pendingFloorId.value = ''
|
||||
loadingFloorId.value = ''
|
||||
requestedFloorId.value = ''
|
||||
requestedFloorLabel.value = ''
|
||||
void indoorRendererRef.value?.showMultiFloor?.()
|
||||
emit('indoorViewChange', 'multi')
|
||||
} else {
|
||||
const floorId = activeFloorId.value
|
||||
pendingFloorId.value = floorId
|
||||
const floorLabel = findFloorItemById(floorId)?.label || floorId
|
||||
if (!floorId) return
|
||||
if (floorId === renderedFloorId.value && props.layerMode !== 'multi') {
|
||||
emit('indoorViewChange', 'floor')
|
||||
emit('layerModeChange', mode)
|
||||
return
|
||||
}
|
||||
|
||||
requestedFloorId.value = floorId
|
||||
requestedFloorLabel.value = floorLabel
|
||||
loadingFloorId.value = floorId
|
||||
const requestSeq = ++floorSwitchRequestSeq
|
||||
activeFloorSwitchRequestSeq.value = requestSeq
|
||||
failedFloorId.value = ''
|
||||
emit('floorRequest', {
|
||||
floorId,
|
||||
floorLabel
|
||||
})
|
||||
Promise.resolve(indoorRendererRef.value?.switchFloor?.(floorId))
|
||||
.then(() => {
|
||||
markFloorSwitchFailedIfUnrendered(floorId, requestSeq)
|
||||
})
|
||||
.catch((error) => {
|
||||
if (isStaleFloorSwitchError(error)) return
|
||||
failedFloorId.value = floorId
|
||||
clearFloorLoadingIfCurrent(floorId)
|
||||
emit('floorSwitchFailed', {
|
||||
floorId,
|
||||
floorLabel
|
||||
})
|
||||
console.error('楼层切换失败:', error)
|
||||
})
|
||||
.finally(() => {
|
||||
if (pendingFloorId.value === floorId) {
|
||||
pendingFloorId.value = ''
|
||||
}
|
||||
})
|
||||
emit('indoorViewChange', 'floor')
|
||||
}
|
||||
|
||||
@@ -622,6 +721,14 @@ const handleMoreTap = () => {
|
||||
}
|
||||
|
||||
const handleThreeFloorChange = (floorId: string) => {
|
||||
renderedFloorId.value = floorId
|
||||
if (loadingFloorId.value === floorId) {
|
||||
renderedFloorSwitchRequestSeq.value = activeFloorSwitchRequestSeq.value
|
||||
}
|
||||
if (failedFloorId.value === floorId) {
|
||||
failedFloorId.value = ''
|
||||
}
|
||||
clearFloorLoadingIfCurrent(floorId)
|
||||
emit('floorChange', floorId)
|
||||
emit('indoorViewChange', 'floor')
|
||||
emit('layerModeChange', 'single')
|
||||
@@ -1120,6 +1227,28 @@ defineExpose({
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.floor-item.pending {
|
||||
pointer-events: none;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.floor-item.pending::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 7px;
|
||||
border: 1px solid rgba(224, 225, 0, 0.9);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.floor-item.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.46;
|
||||
}
|
||||
|
||||
.floor-item.failed:not(.pending) .floor-label {
|
||||
color: #b84a4a;
|
||||
}
|
||||
|
||||
.floor-label {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,9 @@
|
||||
:outdoor-nav-polylines="outdoorNavPolylines"
|
||||
@search-tap="handleGuideSearchTap"
|
||||
@mode-change="handleModeChange"
|
||||
@floor-request="handleFloorRequest"
|
||||
@floor-change="handleFloorChange"
|
||||
@floor-switch-failed="handleFloorSwitchFailed"
|
||||
@indoor-view-change="handleIndoorViewChange"
|
||||
@poi-click="handleGuidePoiClick"
|
||||
@selection-clear="handleGuideSelectionClear"
|
||||
@@ -291,7 +293,12 @@ const initialTopTab = (): GuideTopTab => topTabFromHash() || 'guide'
|
||||
const is3DMode = ref(true)
|
||||
const guideOutdoorState = ref<'home' | 'entrance'>('home')
|
||||
const indoorView = ref<GuideIndoorView>('overview')
|
||||
const activeGuideFloor = ref('1F')
|
||||
const activeGuideFloor = ref('')
|
||||
const requestedFloorId = ref('')
|
||||
const requestedFloorLabel = ref('')
|
||||
const loadingFloorId = ref('')
|
||||
const renderedFloorId = ref('')
|
||||
const failedFloorId = ref('')
|
||||
const selectedGuidePoi = ref<GuideRenderPoi | null>(null)
|
||||
|
||||
// 状态
|
||||
@@ -330,6 +337,12 @@ const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
|
||||
const indoorModelSource = guideUseCase.getModelSource()
|
||||
const guideFloors = ref<MuseumFloor[]>([])
|
||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||
const getGuideFloorById = (floorId: string) => (
|
||||
guideFloors.value.find((floor) => floor.id === floorId)
|
||||
)
|
||||
const getGuideFloorLabel = (floorId: string) => (
|
||||
getGuideFloorById(floorId)?.label || floorId
|
||||
)
|
||||
const indoorLayerMode = computed<LayerDisplayMode>(() => (
|
||||
indoorView.value === 'multi' ? 'multi' : 'single'
|
||||
))
|
||||
@@ -527,10 +540,14 @@ const loadGuideFloors = async () => {
|
||||
try {
|
||||
const floors = await guideUseCase.getFloors()
|
||||
guideFloors.value = floors
|
||||
activeGuideFloor.value = floors.find((floor) => floor.label === activeGuideFloor.value)?.label
|
||||
|| floors.find((floor) => floor.label === '1F')?.label
|
||||
|| floors[0]?.label
|
||||
const normalizedActiveFloorId = activeGuideFloor.value
|
||||
? normalizeGuideFloorId(activeGuideFloor.value)
|
||||
: ''
|
||||
activeGuideFloor.value = floors.find((floor) => floor.id === normalizedActiveFloorId)?.id
|
||||
|| floors.find((floor) => floor.label === '1F')?.id
|
||||
|| floors[0]?.id
|
||||
|| activeGuideFloor.value
|
||||
renderedFloorId.value = activeGuideFloor.value
|
||||
} catch (error) {
|
||||
console.error('加载导览楼层失败:', error)
|
||||
guideFloors.value = []
|
||||
@@ -632,13 +649,36 @@ const handleGuideSearchTap = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleFloorChange = (floor: string) => {
|
||||
activeGuideFloor.value = floor
|
||||
const handleFloorRequest = ({ floorId, floorLabel }: { floorId: string; floorLabel: string }) => {
|
||||
requestedFloorId.value = floorId
|
||||
requestedFloorLabel.value = floorLabel
|
||||
loadingFloorId.value = floorId
|
||||
failedFloorId.value = ''
|
||||
}
|
||||
|
||||
const handleFloorChange = (floorId: string) => {
|
||||
activeGuideFloor.value = floorId
|
||||
renderedFloorId.value = floorId
|
||||
if (loadingFloorId.value === floorId) {
|
||||
loadingFloorId.value = ''
|
||||
}
|
||||
if (failedFloorId.value === floorId) {
|
||||
failedFloorId.value = ''
|
||||
}
|
||||
indoorView.value = 'floor'
|
||||
selectedGuidePoi.value = null
|
||||
isPoiCardCollapsed.value = false
|
||||
showIndoorHint(`已切换到 ${floor},单指旋转、双指可平移`, 3200)
|
||||
console.log('切换楼层:', floor)
|
||||
showIndoorHint(`已切换到 ${getGuideFloorLabel(floorId)},单指旋转、双指可平移`, 3200)
|
||||
console.log('楼层渲染完成:', floorId)
|
||||
}
|
||||
|
||||
const handleFloorSwitchFailed = ({ floorId, floorLabel }: { floorId: string; floorLabel: string }) => {
|
||||
if (loadingFloorId.value === floorId) {
|
||||
loadingFloorId.value = ''
|
||||
}
|
||||
failedFloorId.value = floorId
|
||||
showIndoorHint(`${floorLabel || getGuideFloorLabel(floorId)} 加载失败,请稍后重试`, 3600)
|
||||
console.warn('楼层渲染失败:', floorId)
|
||||
}
|
||||
|
||||
const handleIndoorViewChange = (view: GuideIndoorView) => {
|
||||
@@ -682,8 +722,8 @@ const handleGuidePoiClick = (poi: GuideRenderPoi) => {
|
||||
isPoiCardCollapsed.value = !selectedGuidePoiIsHall.value
|
||||
indoorView.value = 'floor'
|
||||
isSimulatingRoute.value = false
|
||||
const matchedFloor = guideFloors.value.find((floor) => floor.id === poi.floorId)
|
||||
activeGuideFloor.value = matchedFloor?.label || poi.floorId
|
||||
activeGuideFloor.value = poi.floorId
|
||||
renderedFloorId.value = poi.floorId
|
||||
showIndoorHint('已在当前楼层标出点位', 3000)
|
||||
}
|
||||
|
||||
@@ -887,9 +927,9 @@ const handleRoutePickerSearch = ({ keyword }: { mode: 'start' | 'end'; keyword:
|
||||
}
|
||||
|
||||
const focusRouteFloor = (route: GuideRouteResult) => {
|
||||
const floor = guideFloors.value.find((item) => item.id === route.start.floorId)
|
||||
const routeFloorCount = new Set(route.floorSegments.map((segment) => segment.floorId)).size
|
||||
activeGuideFloor.value = floor?.label || route.start.floorLabel
|
||||
activeGuideFloor.value = route.start.floorId
|
||||
renderedFloorId.value = route.start.floorId
|
||||
indoorView.value = routeFloorCount > 1 ? 'multi' : 'floor'
|
||||
}
|
||||
|
||||
@@ -975,7 +1015,13 @@ const guideStatusLabel = computed(() => {
|
||||
if (is3DMode.value) {
|
||||
if (indoorView.value === 'overview') return '建筑外观'
|
||||
if (indoorView.value === 'multi') return '馆内多层'
|
||||
return '馆内单层'
|
||||
if (loadingFloorId.value) {
|
||||
return `切换${requestedFloorLabel.value || getGuideFloorLabel(requestedFloorId.value || loadingFloorId.value)}`
|
||||
}
|
||||
if (failedFloorId.value) {
|
||||
return `${getGuideFloorLabel(failedFloorId.value)}失败`
|
||||
}
|
||||
return getGuideFloorLabel(renderedFloorId.value || activeGuideFloor.value) || '馆内单层'
|
||||
}
|
||||
|
||||
if (guideOutdoorState.value === 'entrance') {
|
||||
|
||||
@@ -126,6 +126,43 @@ const warnSgsGuideModelDiagnostics = (
|
||||
console.warn(`[SGS guide model] ${message}`, payload)
|
||||
}
|
||||
|
||||
const summarizeYValues = (pois: GuideRenderPoi[]) => {
|
||||
const values = pois
|
||||
.map((poi) => poi.positionGltf?.[1])
|
||||
.filter((value): value is number => Number.isFinite(value))
|
||||
.sort((left, right) => left - right)
|
||||
|
||||
if (!values.length) {
|
||||
return {
|
||||
count: 0,
|
||||
min: null,
|
||||
median: null,
|
||||
max: null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
count: values.length,
|
||||
min: values[0],
|
||||
median: values[Math.floor(values.length / 2)],
|
||||
max: values[values.length - 1]
|
||||
}
|
||||
}
|
||||
|
||||
const getMedianPoiY = (pois: GuideRenderPoi[]) => {
|
||||
const values = pois
|
||||
.map((poi) => poi.positionGltf?.[1])
|
||||
.filter((value): value is number => Number.isFinite(value))
|
||||
.sort((left, right) => left - right)
|
||||
|
||||
if (!values.length) return undefined
|
||||
|
||||
const middle = Math.floor(values.length / 2)
|
||||
return values.length % 2
|
||||
? values[middle]
|
||||
: (values[middle - 1] + values[middle]) / 2
|
||||
}
|
||||
|
||||
const getSgsFloorModelMatchKeys = (floor: SgsSdkFloorSummaryPayload, label: string) => (
|
||||
[
|
||||
String(floor.floorId),
|
||||
@@ -285,7 +322,10 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
||||
loadFloorData('navigablePlaces', () => this.provider.getNavigablePlaces(resolvedFloorId))
|
||||
])
|
||||
const ordinaryPois = pois.map((poi) => toSgsRenderPoi(toMuseumPoiFromSgs(poi, manifest.floors)))
|
||||
const museumHallPois = toMuseumHallPoisFromSgs(spaces, navigablePlaces, manifest.floors, resolvedFloorId)
|
||||
const floorPoiMedianY = getMedianPoiY(ordinaryPois)
|
||||
const museumHallPois = toMuseumHallPoisFromSgs(spaces, navigablePlaces, manifest.floors, resolvedFloorId, {
|
||||
fallbackY: floorPoiMedianY
|
||||
})
|
||||
const hallDiagnostics = createSgsHallPoiDiagnostics(spaces, navigablePlaces, museumHallPois)
|
||||
|
||||
if (hallDiagnostics.eligibleSpaceCount > 0 && hallDiagnostics.hallPoiWithPositionCount === 0) {
|
||||
@@ -300,8 +340,21 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
||||
...hallPois,
|
||||
...ordinaryPois
|
||||
])
|
||||
.filter((poi) => poi.floorId === resolvedFloorId)
|
||||
.filter((poi) => Array.isArray(poi.positionGltf) && poi.positionGltf.length === 3)
|
||||
|
||||
const renderHallPois = renderPois.filter((poi) => poi.kind === 'hall' || poi.primaryCategory === 'exhibition_hall')
|
||||
warnSgsGuideModelDiagnostics('floor render POI diagnostics', {
|
||||
floorId: resolvedFloorId,
|
||||
floorCode: matchedFloor.floorCode,
|
||||
poiCount: renderPois.length,
|
||||
hallPoiCount: renderHallPois.length,
|
||||
hallPoiWithPositionCount: renderHallPois.filter((poi) => Boolean(poi.positionGltf)).length,
|
||||
poiY: summarizeYValues(renderPois),
|
||||
hallPoiY: summarizeYValues(renderHallPois),
|
||||
fallbackHallY: floorPoiMedianY ?? null
|
||||
})
|
||||
|
||||
if (hallDiagnostics.hallPoiWithPositionCount > 0 && !renderPois.some((poi) => poi.primaryCategory === 'exhibition_hall')) {
|
||||
warnSgsGuideModelDiagnostics('render POI filter removed all hall POIs', {
|
||||
floorId: resolvedFloorId,
|
||||
|
||||
Reference in New Issue
Block a user