修复馆内导览楼层点位展示
This commit is contained in:
@@ -85,6 +85,7 @@ interface LoadFloorOptions {
|
|||||||
preserveCurrentSceneUntilReady?: boolean
|
preserveCurrentSceneUntilReady?: boolean
|
||||||
suppressProgress?: boolean
|
suppressProgress?: boolean
|
||||||
detachPoiBeforeLoad?: boolean
|
detachPoiBeforeLoad?: boolean
|
||||||
|
allowSameFloorReload?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadModelOptions {
|
interface LoadModelOptions {
|
||||||
@@ -176,6 +177,21 @@ interface PoiMarkerCacheEntry {
|
|||||||
pois: RenderPoi[]
|
pois: RenderPoi[]
|
||||||
group: THREE.Group
|
group: THREE.Group
|
||||||
markerSize: number
|
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 {
|
interface PreparedFloorScene {
|
||||||
@@ -267,6 +283,8 @@ let routeGroup: THREE.Group | null = null
|
|||||||
let activeRoutePreviewSignature = ''
|
let activeRoutePreviewSignature = ''
|
||||||
const poiDataCache = new Map<string, RenderPoi[]>()
|
const poiDataCache = new Map<string, RenderPoi[]>()
|
||||||
const poiMarkerGroupCache = new Map<string, PoiMarkerCacheEntry>()
|
const poiMarkerGroupCache = new Map<string, PoiMarkerCacheEntry>()
|
||||||
|
const poiDisplayOffsetYCache = new Map<string, number>()
|
||||||
|
const poiDisplayDiagnosticsKeys = new Set<string>()
|
||||||
let activeFocusLabelSprite: THREE.Sprite | null = null
|
let activeFocusLabelSprite: THREE.Sprite | null = null
|
||||||
let activeFocusPulseSprite: THREE.Sprite | null = null
|
let activeFocusPulseSprite: THREE.Sprite | null = null
|
||||||
let activeFocusBaseSprite: 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 targetFocusQueue: Promise<unknown> = Promise.resolve()
|
||||||
let modelLoadVersion = 0
|
let modelLoadVersion = 0
|
||||||
let pendingRequestedFloorId = ''
|
let pendingRequestedFloorId = ''
|
||||||
|
let floorSwitchLoadToken = 0
|
||||||
|
let floorSwitchRequestedFloorId = ''
|
||||||
|
let isFloorSwitching = false
|
||||||
let hasLoadedFloorViewOnce = false
|
let hasLoadedFloorViewOnce = false
|
||||||
let pointerDownState: { x: number; y: number } | null = null
|
let pointerDownState: { x: number; y: number } | null = null
|
||||||
let cachedOverviewModel: THREE.Object3D | null = null
|
let cachedOverviewModel: THREE.Object3D | null = null
|
||||||
@@ -631,6 +652,115 @@ const getPoiDisplayMode = (): PoiDisplayMode => {
|
|||||||
|
|
||||||
const getPoiMarkerCacheKey = (floorId: string, displayMode = getPoiDisplayMode()) => `${floorId}:${displayMode}`
|
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) => (
|
const getPoiGlyph = (poi: RenderPoi) => (
|
||||||
poiGlyphMap[poi.iconType] || poi.name.charAt(0) || '点'
|
poiGlyphMap[poi.iconType] || poi.name.charAt(0) || '点'
|
||||||
)
|
)
|
||||||
@@ -850,14 +980,14 @@ const updateAmbientPoiLabels = () => {
|
|||||||
.sort((left, right) => right.priority - left.priority)
|
.sort((left, right) => right.priority - left.priority)
|
||||||
|
|
||||||
candidates.forEach(({ marker, labelSprite, poi }) => {
|
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
|
labelSprite.visible = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAmbientLabelScale(labelSprite, poi)
|
updateAmbientLabelScale(labelSprite, poi)
|
||||||
|
|
||||||
const [x, y, z] = poi.positionGltf
|
|
||||||
const baseOffset = markerSize * (isHallPoi(poi) ? 1.82 : 1.5)
|
const baseOffset = markerSize * (isHallPoi(poi) ? 1.82 : 1.5)
|
||||||
const offsetAttempts = [
|
const offsetAttempts = [
|
||||||
0,
|
0,
|
||||||
@@ -870,7 +1000,7 @@ const updateAmbientPoiLabels = () => {
|
|||||||
let accepted = false
|
let accepted = false
|
||||||
|
|
||||||
for (const offset of offsetAttempts) {
|
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)
|
const screenPosition = getProjectedScreenPosition(labelSprite)
|
||||||
if (!screenPosition) continue
|
if (!screenPosition) continue
|
||||||
|
|
||||||
@@ -885,8 +1015,18 @@ const updateAmbientPoiLabels = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
|
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
|
labelSprite.visible = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,6 +1256,9 @@ const startModelLoad = () => {
|
|||||||
const invalidateModelLoads = () => {
|
const invalidateModelLoads = () => {
|
||||||
modelLoadVersion += 1
|
modelLoadVersion += 1
|
||||||
pendingRequestedFloorId = ''
|
pendingRequestedFloorId = ''
|
||||||
|
floorSwitchLoadToken = 0
|
||||||
|
floorSwitchRequestedFloorId = ''
|
||||||
|
isFloorSwitching = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCurrentModelLoad = (loadToken: number) => (
|
const isCurrentModelLoad = (loadToken: number) => (
|
||||||
@@ -1128,6 +1271,46 @@ const isStaleModelLoadError = (error: unknown) => (
|
|||||||
error instanceof Error && error.message === staleModelLoadMessage
|
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) => {
|
const assertCurrentModelLoad = (loadToken: number, staleObject?: THREE.Object3D) => {
|
||||||
if (isCurrentModelLoad(loadToken)) return
|
if (isCurrentModelLoad(loadToken)) return
|
||||||
|
|
||||||
@@ -1556,8 +1739,9 @@ const detachActivePoiLayer = () => {
|
|||||||
disposeFocusLabel()
|
disposeFocusLabel()
|
||||||
disposeFocusPulse()
|
disposeFocusPulse()
|
||||||
disposeFocusBase()
|
disposeFocusBase()
|
||||||
|
clearFocusHallHighlight()
|
||||||
clearRoutePreview()
|
clearRoutePreview()
|
||||||
clearPoiGroupChildren()
|
detachPoiMarkerGroups()
|
||||||
}
|
}
|
||||||
|
|
||||||
const disposePoiMarkerCache = () => {
|
const disposePoiMarkerCache = () => {
|
||||||
@@ -1567,6 +1751,8 @@ const disposePoiMarkerCache = () => {
|
|||||||
})
|
})
|
||||||
poiMarkerGroupCache.clear()
|
poiMarkerGroupCache.clear()
|
||||||
poiDataCache.clear()
|
poiDataCache.clear()
|
||||||
|
poiDisplayOffsetYCache.clear()
|
||||||
|
poiDisplayDiagnosticsKeys.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearPoiGroupChildren = () => {
|
const clearPoiGroupChildren = () => {
|
||||||
@@ -1679,7 +1865,8 @@ const applyFocusHallMaterial = (material: THREE.Material, seenMaterials: Set<THR
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createFocusHallGlowMesh = (poi: RenderPoi) => {
|
const createFocusHallGlowMesh = (poi: RenderPoi) => {
|
||||||
if (!poi.positionGltf) return null
|
const displayPosition = getPoiDisplayPosition(poi)
|
||||||
|
if (!displayPosition) return null
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
canvas.width = 256
|
canvas.width = 256
|
||||||
@@ -1711,10 +1898,9 @@ const createFocusHallGlowMesh = (poi: RenderPoi) => {
|
|||||||
const hallGlowSize = Math.max(getPoiMarkerSize() * 14, getActiveModelSpan() * 0.14)
|
const hallGlowSize = Math.max(getPoiMarkerSize() * 14, getActiveModelSpan() * 0.14)
|
||||||
const geometry = new THREE.PlaneGeometry(hallGlowSize, hallGlowSize * 0.72)
|
const geometry = new THREE.PlaneGeometry(hallGlowSize, hallGlowSize * 0.72)
|
||||||
const mesh = new THREE.Mesh(geometry, material)
|
const mesh = new THREE.Mesh(geometry, material)
|
||||||
const [x, y, z] = poi.positionGltf
|
|
||||||
|
|
||||||
mesh.name = 'GuideFocusHallGlow'
|
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.rotation.x = -Math.PI / 2
|
||||||
mesh.renderOrder = 7
|
mesh.renderOrder = 7
|
||||||
|
|
||||||
@@ -2363,11 +2549,13 @@ const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
|
|||||||
trigger: 'zoom-in',
|
trigger: 'zoom-in',
|
||||||
distance
|
distance
|
||||||
},
|
},
|
||||||
() => loadFloor(targetFloorId, {
|
async () => {
|
||||||
|
await loadFloor(targetFloorId, {
|
||||||
preserveCurrentSceneUntilReady: hasSceneToKeep,
|
preserveCurrentSceneUntilReady: hasSceneToKeep,
|
||||||
suppressProgress: hasSceneToKeep,
|
suppressProgress: hasSceneToKeep,
|
||||||
detachPoiBeforeLoad: true
|
detachPoiBeforeLoad: true
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
'单楼层模型加载失败',
|
'单楼层模型加载失败',
|
||||||
{ showLoading: !hasSceneToKeep && !canLoadFloorSilently(targetFloorId) }
|
{ showLoading: !hasSceneToKeep && !canLoadFloorSilently(targetFloorId) }
|
||||||
)
|
)
|
||||||
@@ -2436,7 +2624,7 @@ const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
|
|||||||
|
|
||||||
const runAutoSwitchLoad = async (
|
const runAutoSwitchLoad = async (
|
||||||
event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: 'zoom-in' | 'zoom-out'; distance: number },
|
event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: 'zoom-in' | 'zoom-out'; distance: number },
|
||||||
loadTask: () => Promise<void>,
|
loadTask: () => Promise<unknown>,
|
||||||
fallbackMessage: string,
|
fallbackMessage: string,
|
||||||
options: { showLoading?: boolean } = {}
|
options: { showLoading?: boolean } = {}
|
||||||
) => {
|
) => {
|
||||||
@@ -2611,8 +2799,17 @@ const loadCurrentFloorPoiMarkers = async (loadToken: number) => {
|
|||||||
const floor = floorIndex.value.find((item) => item.floorId === currentFloor.value) || floorIndex.value[0]
|
const floor = floorIndex.value.find((item) => item.floorId === currentFloor.value) || floorIndex.value[0]
|
||||||
if (!floor) return
|
if (!floor) return
|
||||||
|
|
||||||
|
if (activeView.value === 'floor') {
|
||||||
await loadFloorPOIs(floor, loadToken)
|
await loadFloorPOIs(floor, loadToken)
|
||||||
assertCurrentModelLoad(loadToken)
|
assertCurrentModelLoad(loadToken)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = await prepareFloorPOIs(floor, loadToken)
|
||||||
|
if (entry) {
|
||||||
|
attachPoiMarkerGroup(entry)
|
||||||
|
}
|
||||||
|
assertCurrentModelLoad(loadToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadCurrentFloorPoiMarkersInBackground = (loadToken: number) => {
|
const loadCurrentFloorPoiMarkersInBackground = (loadToken: number) => {
|
||||||
@@ -2742,7 +2939,7 @@ const prepareFloorScene = async (
|
|||||||
let poiEntry: PoiMarkerCacheEntry | undefined | null = null
|
let poiEntry: PoiMarkerCacheEntry | undefined | null = null
|
||||||
try {
|
try {
|
||||||
poiEntry = shouldRenderPoiMarkers.value
|
poiEntry = shouldRenderPoiMarkers.value
|
||||||
? await prepareFloorPOIs(floor, loadToken)
|
? await prepareFloorPOIs(floor, loadToken, model)
|
||||||
: null
|
: null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (ownsModel) {
|
if (ownsModel) {
|
||||||
@@ -2769,6 +2966,7 @@ const commitPreparedFloorScene = (
|
|||||||
loadToken: number,
|
loadToken: number,
|
||||||
expectedFloorId: string
|
expectedFloorId: string
|
||||||
) => {
|
) => {
|
||||||
|
assertCurrentFloorContextTransaction(loadToken, expectedFloorId, prepared.ownsModel ? prepared.model : undefined)
|
||||||
assertPreparedFloorScene(prepared, loadToken, expectedFloorId)
|
assertPreparedFloorScene(prepared, loadToken, expectedFloorId)
|
||||||
|
|
||||||
const targetScene = scene
|
const targetScene = scene
|
||||||
@@ -2783,6 +2981,7 @@ const commitPreparedFloorScene = (
|
|||||||
currentFloor.value = expectedFloorId
|
currentFloor.value = expectedFloorId
|
||||||
clearSceneData()
|
clearSceneData()
|
||||||
activeModel = prepared.model
|
activeModel = prepared.model
|
||||||
|
activeModel.userData.floorId = expectedFloorId
|
||||||
|
|
||||||
if (prepared.cacheAsSharedModel) {
|
if (prepared.cacheAsSharedModel) {
|
||||||
cachedOverviewModel = activeModel
|
cachedOverviewModel = activeModel
|
||||||
@@ -2798,6 +2997,15 @@ const commitPreparedFloorScene = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
assertCommittedFloorScene(loadToken, expectedFloorId)
|
assertCommittedFloorScene(loadToken, expectedFloorId)
|
||||||
|
|
||||||
|
activeFocusPoiId.value = ''
|
||||||
|
selectedPOI.value = null
|
||||||
|
updatePoiMarkerFocus()
|
||||||
|
hasLoadedFloorViewOnce = true
|
||||||
|
fitCameraToObject(activeModel)
|
||||||
|
markFloorAutoSwitchEntry()
|
||||||
|
refreshPoiVisibilityByDistance()
|
||||||
|
renderRoutePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
|
const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
|
||||||
@@ -2809,24 +3017,35 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
|
|||||||
throw createStaleModelLoadError()
|
throw createStaleModelLoadError()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadToken = startModelLoad()
|
|
||||||
const requestedFloorId = floor.floorId
|
const requestedFloorId = floor.floorId
|
||||||
pendingRequestedFloorId = requestedFloorId
|
if (
|
||||||
if (options.detachPoiBeforeLoad || activeView.value === 'floor' || currentFloor.value !== requestedFloorId) {
|
!options.allowSameFloorReload
|
||||||
|
&& activeView.value === 'floor'
|
||||||
|
&& currentFloor.value === requestedFloorId
|
||||||
|
&& activeModel?.userData.floorId === requestedFloorId
|
||||||
|
&& !isFloorSwitching
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadToken = startFloorContextTransaction(requestedFloorId)
|
||||||
|
if (options.detachPoiBeforeLoad) {
|
||||||
detachActivePoiLayer()
|
detachActivePoiLayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const prepared = await prepareFloorScene(floor, loadToken, options)
|
const prepared = await prepareFloorScene(floor, loadToken, options)
|
||||||
|
assertCurrentFloorContextTransaction(loadToken, requestedFloorId, prepared.ownsModel ? prepared.model : undefined)
|
||||||
commitPreparedFloorScene(prepared, loadToken, requestedFloorId)
|
commitPreparedFloorScene(prepared, loadToken, requestedFloorId)
|
||||||
|
return true
|
||||||
if (!activeModel) throw createStaleModelLoadError()
|
} catch (error) {
|
||||||
assertCommittedFloorScene(loadToken, requestedFloorId)
|
if (!isStaleModelLoadError(error)) {
|
||||||
hasLoadedFloorViewOnce = true
|
restoreCommittedFloorPoiLayer()
|
||||||
fitCameraToObject(activeModel)
|
}
|
||||||
markFloorAutoSwitchEntry()
|
throw error
|
||||||
|
} finally {
|
||||||
refreshPoiVisibilityByDistance()
|
completeFloorContextTransaction(loadToken, requestedFloorId)
|
||||||
renderRoutePreview()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadMultiFloor = async () => {
|
const loadMultiFloor = async () => {
|
||||||
@@ -3290,39 +3509,39 @@ const disposeFocusBase = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const showFocusPoiLabel = (poi: RenderPoi) => {
|
const showFocusPoiLabel = (poi: RenderPoi) => {
|
||||||
if (!poiGroup || !poi.positionGltf) return
|
const displayPosition = getPoiDisplayPosition(poi)
|
||||||
|
if (!poiGroup || !displayPosition) return
|
||||||
|
|
||||||
disposeFocusLabel()
|
disposeFocusLabel()
|
||||||
|
|
||||||
const markerSize = getPoiMarkerSize()
|
const markerSize = getPoiMarkerSize()
|
||||||
const [x, y, z] = poi.positionGltf
|
|
||||||
activeFocusLabelSprite = createPoiLabelSprite(poi, markerSize)
|
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()
|
updateFocusLabelScale()
|
||||||
poiGroup.add(activeFocusLabelSprite)
|
poiGroup.add(activeFocusLabelSprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showFocusPoiBase = (poi: RenderPoi) => {
|
const showFocusPoiBase = (poi: RenderPoi) => {
|
||||||
if (!poiGroup || !poi.positionGltf) return
|
const displayPosition = getPoiDisplayPosition(poi)
|
||||||
|
if (!poiGroup || !displayPosition) return
|
||||||
|
|
||||||
disposeFocusBase()
|
disposeFocusBase()
|
||||||
|
|
||||||
const markerSize = getPoiMarkerSize()
|
const markerSize = getPoiMarkerSize()
|
||||||
const [x, y, z] = poi.positionGltf
|
|
||||||
activeFocusBaseSprite = createPoiBaseSprite(poi, markerSize)
|
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)
|
poiGroup.add(activeFocusBaseSprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showFocusPoiPulse = (poi: RenderPoi) => {
|
const showFocusPoiPulse = (poi: RenderPoi) => {
|
||||||
if (!poiGroup || !poi.positionGltf) return
|
const displayPosition = getPoiDisplayPosition(poi)
|
||||||
|
if (!poiGroup || !displayPosition) return
|
||||||
|
|
||||||
disposeFocusPulse()
|
disposeFocusPulse()
|
||||||
|
|
||||||
const markerSize = getPoiMarkerSize()
|
const markerSize = getPoiMarkerSize()
|
||||||
const [x, y, z] = poi.positionGltf
|
|
||||||
activeFocusPulseSprite = createPoiPulseSprite(poi, markerSize)
|
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)
|
poiGroup.add(activeFocusPulseSprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3462,12 +3681,12 @@ const addFocusMarkerFromRequest = (request: TargetPoiFocusRequest) => {
|
|||||||
if (!poiGroup || findPoiSprite(request.poiId)) return findLoadedPoi(request.poiId) || null
|
if (!poiGroup || findPoiSprite(request.poiId)) return findLoadedPoi(request.poiId) || null
|
||||||
|
|
||||||
const poi = createPoiFromFocusRequest(request)
|
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 markerSize = getPoiMarkerSize()
|
||||||
const { sprite, hitTarget } = createPoiMarkerSprites(poi, markerSize)
|
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)
|
hitTarget.position.copy(sprite.position)
|
||||||
poiGroup.add(sprite)
|
poiGroup.add(sprite)
|
||||||
poiGroup.add(hitTarget)
|
poiGroup.add(hitTarget)
|
||||||
@@ -3488,12 +3707,14 @@ const createPoiMarkerGroup = (
|
|||||||
group.userData.displayMode = displayMode
|
group.userData.displayMode = displayMode
|
||||||
|
|
||||||
pois.forEach((poi) => {
|
pois.forEach((poi) => {
|
||||||
const [x, y, z] = poi.positionGltf!
|
const displayPosition = getPoiDisplayPosition(poi, floorId)
|
||||||
|
if (!displayPosition) return
|
||||||
|
|
||||||
const { sprite, hitTarget, labelSprite } = createPoiMarkerSprites(poi, markerSize)
|
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)
|
hitTarget.position.copy(sprite.position)
|
||||||
if (labelSprite) {
|
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
|
labelSprite.visible = false
|
||||||
}
|
}
|
||||||
sprite.visible = shouldShowPoiAtDistance(poi, getPoiVisibilityTier())
|
sprite.visible = shouldShowPoiAtDistance(poi, getPoiVisibilityTier())
|
||||||
@@ -3520,7 +3741,39 @@ const attachPoiMarkerGroup = (entry: PoiMarkerCacheEntry) => {
|
|||||||
updatePoiMarkerFocus()
|
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
|
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) return
|
||||||
|
|
||||||
const displayMode = getPoiDisplayMode()
|
const displayMode = getPoiDisplayMode()
|
||||||
@@ -3536,14 +3789,18 @@ const prepareFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
|
|||||||
|
|
||||||
poiDataCache.set(floor.floorId, floorPois)
|
poiDataCache.set(floor.floorId, floorPois)
|
||||||
const validPois = floorPois.filter((poi) => shouldShowPoiInCurrentMode(poi))
|
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 markerGroup = createPoiMarkerGroup(floor.floorId, displayMode, validPois, markerSize)
|
||||||
const entry: PoiMarkerCacheEntry = {
|
const entry: PoiMarkerCacheEntry = {
|
||||||
floorId: floor.floorId,
|
floorId: floor.floorId,
|
||||||
displayMode,
|
displayMode,
|
||||||
pois: validPois,
|
pois: validPois,
|
||||||
group: markerGroup,
|
group: markerGroup,
|
||||||
markerSize
|
markerSize,
|
||||||
|
displayOffsetY
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
||||||
@@ -3727,10 +3984,10 @@ const isSceneReadyForTargetFocus = () => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const focusCameraOnPoi = (poi: RenderPoi) => {
|
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 = displayPosition.clone()
|
||||||
const target = new THREE.Vector3(x, y, z)
|
|
||||||
const modelSize = activeModel
|
const modelSize = activeModel
|
||||||
? new THREE.Box3().setFromObject(activeModel).getSize(new THREE.Vector3())
|
? new THREE.Box3().setFromObject(activeModel).getSize(new THREE.Vector3())
|
||||||
: new THREE.Vector3(80, 80, 80)
|
: new THREE.Vector3(80, 80, 80)
|
||||||
@@ -3889,26 +4146,46 @@ const init3DScene = async () => {
|
|||||||
|
|
||||||
const handleFloorChange = async (floorId: string) => {
|
const handleFloorChange = async (floorId: string) => {
|
||||||
const requestedFloorId = resolveFloorIdFromRequest(floorId) || floorId
|
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 {
|
try {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
loadError.value = false
|
loadError.value = false
|
||||||
setProgress(12, `正在切换到 ${formatFloorLabel(requestedFloorId)}...`)
|
setProgress(12, `正在切换到 ${formatFloorLabel(requestedFloorId)}...`)
|
||||||
await loadFloor(requestedFloorId, {
|
const didCommit = await loadFloor(requestedFloorId, {
|
||||||
preserveCurrentSceneUntilReady: true,
|
preserveCurrentSceneUntilReady: true,
|
||||||
suppressProgress: false,
|
suppressProgress: false,
|
||||||
detachPoiBeforeLoad: true
|
detachPoiBeforeLoad: true
|
||||||
})
|
})
|
||||||
updatePoiVisibilityByDistance()
|
updatePoiVisibilityByDistance()
|
||||||
isLoading.value = false
|
if (didCommit) {
|
||||||
emit('floorChange', requestedFloorId)
|
emit('floorChange', requestedFloorId)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isStaleModelLoadError(error)) return
|
if (isStaleModelLoadError(error)) return
|
||||||
|
|
||||||
console.error('楼层模型加载失败:', error)
|
console.error('楼层模型加载失败:', error)
|
||||||
loadError.value = true
|
loadError.value = true
|
||||||
isLoading.value = false
|
restoreCommittedFloorPoiLayer()
|
||||||
setProgress(0, error instanceof Error ? error.message : '楼层模型加载失败')
|
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"
|
:id="floor.scrollId"
|
||||||
:key="floor.id"
|
:key="floor.id"
|
||||||
class="floor-item"
|
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)"
|
@tap.stop="handleFloorChange(floor)"
|
||||||
>
|
>
|
||||||
<text class="floor-label">{{ floor.label }}</text>
|
<text class="floor-label">{{ floor.label }}</text>
|
||||||
@@ -247,6 +252,11 @@ interface GuideFloorOption {
|
|||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FloorSwitchEvent {
|
||||||
|
floorId: string
|
||||||
|
floorLabel: string
|
||||||
|
}
|
||||||
|
|
||||||
interface OutdoorNavPolyline {
|
interface OutdoorNavPolyline {
|
||||||
points: Array<{ latitude: number; longitude: number }>
|
points: Array<{ latitude: number; longitude: number }>
|
||||||
color: string
|
color: string
|
||||||
@@ -381,7 +391,9 @@ const props = withDefaults(defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
searchTap: []
|
searchTap: []
|
||||||
modeChange: [mode: '2d' | '3d']
|
modeChange: [mode: '2d' | '3d']
|
||||||
|
floorRequest: [event: FloorSwitchEvent]
|
||||||
floorChange: [floor: string]
|
floorChange: [floor: string]
|
||||||
|
floorSwitchFailed: [event: FloorSwitchEvent]
|
||||||
toolClick: [tool: string]
|
toolClick: [tool: string]
|
||||||
moreClick: []
|
moreClick: []
|
||||||
indoorViewChange: [view: IndoorViewMode]
|
indoorViewChange: [view: IndoorViewMode]
|
||||||
@@ -445,8 +457,15 @@ const activeFloorId = computed(() => {
|
|||||||
return normalizedFloor?.id || normalizedFloorId
|
return normalizedFloor?.id || normalizedFloorId
|
||||||
})
|
})
|
||||||
const activeFloorScrollIntoView = ref('')
|
const activeFloorScrollIntoView = ref('')
|
||||||
const pendingFloorId = ref('')
|
const requestedFloorId = ref('')
|
||||||
let suppressFloorAutoScrollUntil = 0
|
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 = () => {
|
const syncActiveFloorScroll = () => {
|
||||||
if (props.mapType !== 'indoor' || props.indoorView === 'overview' || props.layerMode === 'multi') {
|
if (props.mapType !== 'indoor' || props.indoorView === 'overview' || props.layerMode === 'multi') {
|
||||||
@@ -454,10 +473,12 @@ const syncActiveFloorScroll = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() < suppressFloorAutoScrollUntil) {
|
if (activeFloorId.value === suppressFloorAutoScrollForFloorId) {
|
||||||
activeFloorScrollIntoView.value = ''
|
activeFloorScrollIntoView.value = ''
|
||||||
|
suppressFloorAutoScrollForFloorId = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
suppressFloorAutoScrollForFloorId = ''
|
||||||
|
|
||||||
const matchedFloor = floorItems.value.find((floor) => floor.id === activeFloorId.value)
|
const matchedFloor = floorItems.value.find((floor) => floor.id === activeFloorId.value)
|
||||||
activeFloorScrollIntoView.value = matchedFloor?.scrollId || ''
|
activeFloorScrollIntoView.value = matchedFloor?.scrollId || ''
|
||||||
@@ -475,6 +496,11 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(activeFloorId, (floorId) => {
|
||||||
|
if (!floorId || loadingFloorId.value === floorId) return
|
||||||
|
renderedFloorId.value = floorId
|
||||||
|
})
|
||||||
|
|
||||||
const searchFieldStyle = computed(() => ({
|
const searchFieldStyle = computed(() => ({
|
||||||
top: props.searchTop
|
top: props.searchTop
|
||||||
}))
|
}))
|
||||||
@@ -528,26 +554,76 @@ const isStaleFloorSwitchError = (error: unknown) => (
|
|||||||
error instanceof Error && error.message === 'STALE_MODEL_LOAD'
|
error instanceof Error && error.message === 'STALE_MODEL_LOAD'
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFloorChange = (floor: { id: string; label: string }) => {
|
const findFloorItemById = (floorId: string) => (
|
||||||
const floorId = floor.id || props.normalizeFloorId(floor.label)
|
floorItems.value.find((floor) => floor.id === floorId)
|
||||||
if (!floorId || pendingFloorId.value === floorId) return
|
)
|
||||||
|
|
||||||
pendingFloorId.value = floorId
|
const isFloorSwitchDisabled = (floorId: string) => (
|
||||||
suppressFloorAutoScrollUntil = Date.now() + 650
|
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 = ''
|
activeFloorScrollIntoView.value = ''
|
||||||
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
|
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
|
||||||
|
emit('floorRequest', {
|
||||||
|
floorId,
|
||||||
|
floorLabel: floor.label
|
||||||
|
})
|
||||||
emit('indoorViewChange', 'floor')
|
emit('indoorViewChange', 'floor')
|
||||||
emit('layerModeChange', 'single')
|
emit('layerModeChange', 'single')
|
||||||
|
|
||||||
Promise.resolve(indoorRendererRef.value?.switchFloor?.(floorId))
|
Promise.resolve(indoorRendererRef.value?.switchFloor?.(floorId))
|
||||||
|
.then(() => {
|
||||||
|
markFloorSwitchFailedIfUnrendered(floorId, requestSeq)
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (isStaleFloorSwitchError(error)) return
|
if (isStaleFloorSwitchError(error)) return
|
||||||
console.error('楼层切换失败:', error)
|
failedFloorId.value = floorId
|
||||||
|
clearFloorLoadingIfCurrent(floorId)
|
||||||
|
emit('floorSwitchFailed', {
|
||||||
|
floorId,
|
||||||
|
floorLabel: floor.label
|
||||||
})
|
})
|
||||||
.finally(() => {
|
console.error('楼层切换失败:', error)
|
||||||
if (pendingFloorId.value === floorId) {
|
|
||||||
pendingFloorId.value = ''
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,21 +632,44 @@ const handleLayerModeChange = (mode: LayerDisplayMode) => {
|
|||||||
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
|
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
|
||||||
|
|
||||||
if (mode === 'multi') {
|
if (mode === 'multi') {
|
||||||
pendingFloorId.value = ''
|
loadingFloorId.value = ''
|
||||||
|
requestedFloorId.value = ''
|
||||||
|
requestedFloorLabel.value = ''
|
||||||
void indoorRendererRef.value?.showMultiFloor?.()
|
void indoorRendererRef.value?.showMultiFloor?.()
|
||||||
emit('indoorViewChange', 'multi')
|
emit('indoorViewChange', 'multi')
|
||||||
} else {
|
} else {
|
||||||
const floorId = activeFloorId.value
|
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))
|
Promise.resolve(indoorRendererRef.value?.switchFloor?.(floorId))
|
||||||
|
.then(() => {
|
||||||
|
markFloorSwitchFailedIfUnrendered(floorId, requestSeq)
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (isStaleFloorSwitchError(error)) return
|
if (isStaleFloorSwitchError(error)) return
|
||||||
console.error('楼层切换失败:', error)
|
failedFloorId.value = floorId
|
||||||
|
clearFloorLoadingIfCurrent(floorId)
|
||||||
|
emit('floorSwitchFailed', {
|
||||||
|
floorId,
|
||||||
|
floorLabel
|
||||||
})
|
})
|
||||||
.finally(() => {
|
console.error('楼层切换失败:', error)
|
||||||
if (pendingFloorId.value === floorId) {
|
|
||||||
pendingFloorId.value = ''
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
emit('indoorViewChange', 'floor')
|
emit('indoorViewChange', 'floor')
|
||||||
}
|
}
|
||||||
@@ -622,6 +721,14 @@ const handleMoreTap = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleThreeFloorChange = (floorId: string) => {
|
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('floorChange', floorId)
|
||||||
emit('indoorViewChange', 'floor')
|
emit('indoorViewChange', 'floor')
|
||||||
emit('layerModeChange', 'single')
|
emit('layerModeChange', 'single')
|
||||||
@@ -1120,6 +1227,28 @@ defineExpose({
|
|||||||
background: #000000;
|
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 {
|
.floor-label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
|||||||
@@ -209,6 +209,23 @@ const exhibitionHallKeywords = [
|
|||||||
'家园'
|
'家园'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const nonHallNavigablePlaceTypes = new Set([
|
||||||
|
'elevator',
|
||||||
|
'stairs',
|
||||||
|
'stair',
|
||||||
|
'escalator',
|
||||||
|
'lift',
|
||||||
|
'toilet',
|
||||||
|
'accessible_toilet',
|
||||||
|
'restroom',
|
||||||
|
'卫生间',
|
||||||
|
'洗手间',
|
||||||
|
'无障碍卫生间',
|
||||||
|
'电梯',
|
||||||
|
'楼梯',
|
||||||
|
'扶梯'
|
||||||
|
])
|
||||||
|
|
||||||
export interface SgsHallPoiDiagnostics {
|
export interface SgsHallPoiDiagnostics {
|
||||||
spaceCount: number
|
spaceCount: number
|
||||||
eligibleSpaceCount: number
|
eligibleSpaceCount: number
|
||||||
@@ -220,6 +237,14 @@ export interface SgsHallPoiDiagnostics {
|
|||||||
skippedPlaceCount: number
|
skippedPlaceCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SgsHallPoiBuildOptions {
|
||||||
|
fallbackY?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const sgsSpaceCenterConfidence = 'backend-sgs-sdk-space-center'
|
||||||
|
const sgsSpaceBoundaryCenterConfidence = 'backend-sgs-sdk-space-boundary-center'
|
||||||
|
const sgsHallEntranceConfidence = 'backend-sgs-sdk-hall-entrance'
|
||||||
|
|
||||||
const hasExhibitionKeyword = (value?: string | null) => (
|
const hasExhibitionKeyword = (value?: string | null) => (
|
||||||
exhibitionHallKeywords.some((keyword) => normalizedText(value).includes(keyword))
|
exhibitionHallKeywords.some((keyword) => normalizedText(value).includes(keyword))
|
||||||
)
|
)
|
||||||
@@ -233,13 +258,60 @@ const searchableSpaceText = (space: SgsSpacePayload) => [
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
|
|
||||||
const normalizeSpaceCenter = (space: SgsSpacePayload): [number, number, number] | undefined => normalizePositionSource(
|
const parseBoundaryWktCenter = (
|
||||||
space.center || {
|
boundaryWkt?: string | null,
|
||||||
x: undefined,
|
fallbackY?: number
|
||||||
y: undefined,
|
): [number, number, number] | undefined => {
|
||||||
z: undefined
|
const matches = [...(boundaryWkt || '').matchAll(/(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)/g)]
|
||||||
|
const points = matches
|
||||||
|
.map((match) => [Number(match[1]), Number(match[2])] as const)
|
||||||
|
.filter(([x, z]) => Number.isFinite(x) && Number.isFinite(z))
|
||||||
|
|
||||||
|
if (!points.length || !Number.isFinite(fallbackY)) return undefined
|
||||||
|
|
||||||
|
const xs = points.map(([x]) => x)
|
||||||
|
const zs = points.map(([, z]) => z)
|
||||||
|
const x = (Math.min(...xs) + Math.max(...xs)) / 2
|
||||||
|
const z = (Math.min(...zs) + Math.max(...zs)) / 2
|
||||||
|
|
||||||
|
return [x, fallbackY as number, z]
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizePositionSourceWithFallbackY = (
|
||||||
|
source: SgsPositionPayload | null | undefined,
|
||||||
|
fallbackY?: number
|
||||||
|
): [number, number, number] | undefined => {
|
||||||
|
if (!source) return undefined
|
||||||
|
|
||||||
|
const x = Number(source.x)
|
||||||
|
const rawY = Number(source.y)
|
||||||
|
const y = Number.isFinite(rawY) ? rawY : Number(fallbackY)
|
||||||
|
const z = Number(source.z)
|
||||||
|
|
||||||
|
if (![x, y, z].every(Number.isFinite)) return undefined
|
||||||
|
return [x, y, z]
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeSpacePosition = (
|
||||||
|
space: SgsSpacePayload,
|
||||||
|
fallbackY?: number
|
||||||
|
) => {
|
||||||
|
const centerPosition = normalizePositionSourceWithFallbackY(space.center, fallbackY)
|
||||||
|
if (centerPosition) {
|
||||||
|
return {
|
||||||
|
position: centerPosition,
|
||||||
|
sourceConfidence: sgsSpaceCenterConfidence
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
const boundaryCenter = parseBoundaryWktCenter(space.boundaryWkt, fallbackY)
|
||||||
|
return boundaryCenter
|
||||||
|
? {
|
||||||
|
position: boundaryCenter,
|
||||||
|
sourceConfidence: sgsSpaceBoundaryCenterConfidence
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
export const isExhibitionHallSpace = (space: SgsSpacePayload) => {
|
export const isExhibitionHallSpace = (space: SgsSpacePayload) => {
|
||||||
const type = normalizedText(space.type).toLowerCase()
|
const type = normalizedText(space.type).toLowerCase()
|
||||||
@@ -254,6 +326,17 @@ export const isExhibitionHallSpace = (space: SgsSpacePayload) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isExhibitionHallNavigablePlace = (place: SgsNavigablePlacePayload) => {
|
export const isExhibitionHallNavigablePlace = (place: SgsNavigablePlacePayload) => {
|
||||||
|
const placeTypes = [
|
||||||
|
place.category,
|
||||||
|
place.type,
|
||||||
|
place.typeCode,
|
||||||
|
place.typeName
|
||||||
|
]
|
||||||
|
.map((value) => normalizedText(value).toLowerCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
if (placeTypes.some((type) => nonHallNavigablePlaceTypes.has(type))) return false
|
||||||
|
|
||||||
const searchableText = [
|
const searchableText = [
|
||||||
place.name,
|
place.name,
|
||||||
place.ownerName,
|
place.ownerName,
|
||||||
@@ -280,18 +363,22 @@ const normalizePosition = (poi: SgsPoiPayload): [number, number, number] | undef
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const normalizePlacePosition = (place: SgsNavigablePlacePayload): [number, number, number] | undefined => normalizePositionSource(
|
const normalizePlacePosition = (
|
||||||
|
place: SgsNavigablePlacePayload,
|
||||||
|
fallbackY?: number
|
||||||
|
): [number, number, number] | undefined => normalizePositionSourceWithFallbackY(
|
||||||
place.position
|
place.position
|
||||||
? {
|
? {
|
||||||
x: place.position.x,
|
x: place.position.x,
|
||||||
y: place.position.y ?? 0,
|
y: place.position.y,
|
||||||
z: place.position.z
|
z: place.position.z
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
x: place.x,
|
x: place.x,
|
||||||
y: place.y ?? 0,
|
y: place.y,
|
||||||
z: place.z
|
z: place.z
|
||||||
}
|
},
|
||||||
|
fallbackY
|
||||||
)
|
)
|
||||||
|
|
||||||
const categoryFor = (poi: SgsPoiPayload): MuseumCategory & { accessible?: boolean } => {
|
const categoryFor = (poi: SgsPoiPayload): MuseumCategory & { accessible?: boolean } => {
|
||||||
@@ -418,7 +505,8 @@ const floorLabelForSgs = (
|
|||||||
const createHallEntrance = (
|
const createHallEntrance = (
|
||||||
place: SgsNavigablePlacePayload,
|
place: SgsNavigablePlacePayload,
|
||||||
floors: SgsSdkFloorSummaryPayload[],
|
floors: SgsSdkFloorSummaryPayload[],
|
||||||
fallbackFloorId: string
|
fallbackFloorId: string,
|
||||||
|
fallbackY?: number
|
||||||
) => {
|
) => {
|
||||||
const floorId = stringifyId(place.floorId) || fallbackFloorId
|
const floorId = stringifyId(place.floorId) || fallbackFloorId
|
||||||
const placeId = stringifyId(place.id || place.nodeId)
|
const placeId = stringifyId(place.id || place.nodeId)
|
||||||
@@ -429,7 +517,7 @@ const createHallEntrance = (
|
|||||||
name: normalizedText(place.name) || normalizedText(place.ownerName) || '展厅出入口',
|
name: normalizedText(place.name) || normalizedText(place.ownerName) || '展厅出入口',
|
||||||
floorId,
|
floorId,
|
||||||
floorLabel: floorLabelForSgs(floorId, floors, place.floorCode, place.floorName),
|
floorLabel: floorLabelForSgs(floorId, floors, place.floorCode, place.floorName),
|
||||||
positionGltf: normalizePlacePosition(place),
|
positionGltf: normalizePlacePosition(place, fallbackY),
|
||||||
sourceObjectName: routeNodeId || undefined,
|
sourceObjectName: routeNodeId || undefined,
|
||||||
sourcePlaceId: routeNodeId ? placeId || undefined : undefined,
|
sourcePlaceId: routeNodeId ? placeId || undefined : undefined,
|
||||||
routeNodeId
|
routeNodeId
|
||||||
@@ -437,27 +525,27 @@ const createHallEntrance = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createHallPoiId = (
|
const createHallPoiId = (
|
||||||
space: SgsSpacePayload | undefined,
|
space: SgsSpacePayload
|
||||||
place: SgsNavigablePlacePayload,
|
|
||||||
fallbackIndex: number
|
|
||||||
) => {
|
) => {
|
||||||
const spaceId = stringifyId(space?.id)
|
const spaceId = stringifyId(space.id)
|
||||||
if (spaceId) return `hall-${spaceId}`
|
if (spaceId) return `hall-${spaceId}`
|
||||||
|
|
||||||
const ownerName = normalizedHallName(placeHallName(place))
|
const ownerName = normalizedHallName(space.name)
|
||||||
const floorIdentity = normalizedHallName(stringifyId(place.floorId) || place.floorCode || 'unknown-floor')
|
const floorIdentity = normalizedHallName(stringifyId(space.floorId) || 'unknown-floor')
|
||||||
if (ownerName) return `hall-place-${floorIdentity}-${ownerName}`
|
if (ownerName) return `hall-space-${floorIdentity}-${ownerName}`
|
||||||
|
|
||||||
return `hall-place-${floorIdentity}-${stringifyId(place.id) || fallbackIndex + 1}`
|
return `hall-space-${floorIdentity}-unknown`
|
||||||
}
|
}
|
||||||
|
|
||||||
const createSpaceFallbackHallPoi = (
|
const createSpaceFallbackHallPoi = (
|
||||||
space: SgsSpacePayload,
|
space: SgsSpacePayload,
|
||||||
floors: SgsSdkFloorSummaryPayload[],
|
floors: SgsSdkFloorSummaryPayload[],
|
||||||
fallbackFloorId: string
|
fallbackFloorId: string,
|
||||||
|
fallbackY?: number
|
||||||
): MuseumPoi | null => {
|
): MuseumPoi | null => {
|
||||||
const floorId = stringifyId(space.floorId) || fallbackFloorId
|
const floorId = stringifyId(space.floorId) || fallbackFloorId
|
||||||
const position = normalizeSpaceCenter(space)
|
const spacePosition = normalizeSpacePosition(space, fallbackY)
|
||||||
|
const position = spacePosition?.position
|
||||||
const hallId = stringifyId(space.id)
|
const hallId = stringifyId(space.id)
|
||||||
|
|
||||||
if (!hallId || !normalizedText(space.name) || !position) return null
|
if (!hallId || !normalizedText(space.name) || !position) return null
|
||||||
@@ -473,7 +561,7 @@ const createSpaceFallbackHallPoi = (
|
|||||||
],
|
],
|
||||||
positionGltf: position,
|
positionGltf: position,
|
||||||
sourceObjectName: space.sourceNodeName || undefined,
|
sourceObjectName: space.sourceNodeName || undefined,
|
||||||
sourceConfidence: 'backend-sgs-sdk-space-center',
|
sourceConfidence: spacePosition.sourceConfidence,
|
||||||
navigationReadiness: '位置预览',
|
navigationReadiness: '位置预览',
|
||||||
accessible: false,
|
accessible: false,
|
||||||
kind: 'hall',
|
kind: 'hall',
|
||||||
@@ -489,24 +577,28 @@ export const toMuseumHallPoisFromSgs = (
|
|||||||
spaces: SgsSpacePayload[],
|
spaces: SgsSpacePayload[],
|
||||||
navigablePlaces: SgsNavigablePlacePayload[],
|
navigablePlaces: SgsNavigablePlacePayload[],
|
||||||
floors: SgsSdkFloorSummaryPayload[],
|
floors: SgsSdkFloorSummaryPayload[],
|
||||||
fallbackFloorId: string
|
fallbackFloorId: string,
|
||||||
|
options: SgsHallPoiBuildOptions = {}
|
||||||
): MuseumPoi[] => {
|
): MuseumPoi[] => {
|
||||||
const hallSpaces = spaces.filter(isExhibitionHallSpace)
|
const hallSpaces = spaces.filter(isExhibitionHallSpace)
|
||||||
const hallPlaces = navigablePlaces.filter(isExhibitionHallNavigablePlace)
|
const hallPlaces = navigablePlaces.filter(isExhibitionHallNavigablePlace)
|
||||||
const halls = new Map<string, MuseumPoi>()
|
const halls = new Map<string, MuseumPoi>()
|
||||||
|
|
||||||
hallPlaces.forEach((place, index) => {
|
hallPlaces.forEach((place) => {
|
||||||
const matchedSpace = findMatchedHallSpace(place, hallSpaces)
|
const matchedSpace = findMatchedHallSpace(place, hallSpaces)
|
||||||
const entrance = createHallEntrance(place, floors, fallbackFloorId)
|
if (!matchedSpace) return
|
||||||
const entranceHasRouteAnchor = Boolean(entrance.positionGltf && entrance.routeNodeId)
|
|
||||||
const fallbackPosition = matchedSpace ? normalizeSpaceCenter(matchedSpace) : undefined
|
const entrance = createHallEntrance(place, floors, fallbackFloorId, options.fallbackY)
|
||||||
|
const spacePosition = matchedSpace ? normalizeSpacePosition(matchedSpace, options.fallbackY) : undefined
|
||||||
|
const spaceCenter = spacePosition?.position
|
||||||
|
const entrancePosition = entrance.positionGltf
|
||||||
const fallbackFloorIdForPoi = stringifyId(matchedSpace?.floorId) || entrance.floorId || fallbackFloorId
|
const fallbackFloorIdForPoi = stringifyId(matchedSpace?.floorId) || entrance.floorId || fallbackFloorId
|
||||||
const fallbackFloorLabel = floorLabelForSgs(
|
const fallbackFloorLabel = floorLabelForSgs(
|
||||||
fallbackFloorIdForPoi,
|
fallbackFloorIdForPoi,
|
||||||
floors
|
floors
|
||||||
)
|
)
|
||||||
const hallId = stringifyId(matchedSpace?.id) || undefined
|
const hallId = stringifyId(matchedSpace?.id) || undefined
|
||||||
const poiId = createHallPoiId(matchedSpace, place, index)
|
const poiId = createHallPoiId(matchedSpace)
|
||||||
const hallName = normalizedText(matchedSpace?.name) || placeHallName(place) || entrance.name
|
const hallName = normalizedText(matchedSpace?.name) || placeHallName(place) || entrance.name
|
||||||
const existing = halls.get(poiId)
|
const existing = halls.get(poiId)
|
||||||
|
|
||||||
@@ -515,53 +607,54 @@ export const toMuseumHallPoisFromSgs = (
|
|||||||
...(existing.entrances || []),
|
...(existing.entrances || []),
|
||||||
entrance
|
entrance
|
||||||
]
|
]
|
||||||
if (entranceHasRouteAnchor) {
|
if (spaceCenter) {
|
||||||
existing.positionGltf = entrance.positionGltf
|
existing.positionGltf = spaceCenter
|
||||||
|
existing.floorId = fallbackFloorIdForPoi
|
||||||
|
existing.floorLabel = fallbackFloorLabel
|
||||||
|
existing.sourceObjectName = matchedSpace?.sourceNodeName || undefined
|
||||||
|
existing.sourceConfidence = spacePosition.sourceConfidence
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing.positionGltf && entrancePosition) {
|
||||||
|
existing.positionGltf = entrancePosition
|
||||||
existing.floorId = entrance.floorId
|
existing.floorId = entrance.floorId
|
||||||
existing.floorLabel = entrance.floorLabel
|
existing.floorLabel = entrance.floorLabel
|
||||||
existing.sourceObjectName = entrance.sourceObjectName
|
existing.sourceObjectName = entrance.sourceObjectName
|
||||||
existing.sourcePlaceId = entrance.sourcePlaceId
|
existing.sourcePlaceId = entrance.sourcePlaceId
|
||||||
existing.sourceConfidence = 'backend-sgs-sdk-hall-entrance'
|
existing.sourceConfidence = sgsHallEntranceConfidence
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existing.positionGltf && fallbackPosition) {
|
|
||||||
existing.positionGltf = fallbackPosition
|
|
||||||
existing.floorId = fallbackFloorIdForPoi
|
|
||||||
existing.floorLabel = fallbackFloorLabel
|
|
||||||
existing.sourceObjectName = matchedSpace?.sourceNodeName || undefined
|
|
||||||
existing.sourceConfidence = 'backend-sgs-sdk-space-center'
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const positionGltf = entranceHasRouteAnchor ? entrance.positionGltf : fallbackPosition
|
const usesSpaceCenter = Boolean(spaceCenter)
|
||||||
|
const positionGltf = spaceCenter || entrancePosition
|
||||||
if (!positionGltf) return
|
if (!positionGltf) return
|
||||||
|
|
||||||
halls.set(poiId, {
|
halls.set(poiId, {
|
||||||
id: poiId,
|
id: poiId,
|
||||||
name: hallName,
|
name: hallName,
|
||||||
floorId: entranceHasRouteAnchor ? entrance.floorId : fallbackFloorIdForPoi,
|
floorId: usesSpaceCenter ? fallbackFloorIdForPoi : entrance.floorId,
|
||||||
floorLabel: entranceHasRouteAnchor ? entrance.floorLabel : fallbackFloorLabel,
|
floorLabel: usesSpaceCenter ? fallbackFloorLabel : entrance.floorLabel,
|
||||||
primaryCategory: hallCategory,
|
primaryCategory: hallCategory,
|
||||||
categories: [
|
categories: [
|
||||||
hallCategory,
|
hallCategory,
|
||||||
hallEntranceCategory
|
hallEntranceCategory
|
||||||
],
|
],
|
||||||
positionGltf,
|
positionGltf,
|
||||||
sourceObjectName: entranceHasRouteAnchor
|
sourceObjectName: usesSpaceCenter
|
||||||
? entrance.sourceObjectName
|
? matchedSpace?.sourceNodeName || undefined
|
||||||
: matchedSpace?.sourceNodeName || undefined,
|
: entrance.sourceObjectName,
|
||||||
sourceConfidence: entranceHasRouteAnchor
|
sourceConfidence: usesSpaceCenter
|
||||||
? 'backend-sgs-sdk-hall-entrance'
|
? spacePosition?.sourceConfidence || sgsSpaceCenterConfidence
|
||||||
: 'backend-sgs-sdk-space-center',
|
: sgsHallEntranceConfidence,
|
||||||
navigationReadiness: '位置预览',
|
navigationReadiness: '位置预览',
|
||||||
accessible: false,
|
accessible: false,
|
||||||
kind: 'hall',
|
kind: 'hall',
|
||||||
hallId,
|
hallId,
|
||||||
hallName,
|
hallName,
|
||||||
spaceId: hallId,
|
spaceId: hallId,
|
||||||
sourcePlaceId: entranceHasRouteAnchor ? entrance.sourcePlaceId : undefined,
|
sourcePlaceId: usesSpaceCenter ? undefined : entrance.sourcePlaceId,
|
||||||
sourceSpaceId: hallId,
|
sourceSpaceId: hallId,
|
||||||
entrances: [entrance]
|
entrances: [entrance]
|
||||||
})
|
})
|
||||||
@@ -571,7 +664,7 @@ export const toMuseumHallPoisFromSgs = (
|
|||||||
const hallId = stringifyId(space.id)
|
const hallId = stringifyId(space.id)
|
||||||
if (!hallId || halls.has(`hall-${hallId}`)) return
|
if (!hallId || halls.has(`hall-${hallId}`)) return
|
||||||
|
|
||||||
const fallbackPoi = createSpaceFallbackHallPoi(space, floors, fallbackFloorId)
|
const fallbackPoi = createSpaceFallbackHallPoi(space, floors, fallbackFloorId, options.fallbackY)
|
||||||
if (fallbackPoi) {
|
if (fallbackPoi) {
|
||||||
halls.set(fallbackPoi.id, fallbackPoi)
|
halls.set(fallbackPoi.id, fallbackPoi)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,9 @@
|
|||||||
:outdoor-nav-polylines="outdoorNavPolylines"
|
:outdoor-nav-polylines="outdoorNavPolylines"
|
||||||
@search-tap="handleGuideSearchTap"
|
@search-tap="handleGuideSearchTap"
|
||||||
@mode-change="handleModeChange"
|
@mode-change="handleModeChange"
|
||||||
|
@floor-request="handleFloorRequest"
|
||||||
@floor-change="handleFloorChange"
|
@floor-change="handleFloorChange"
|
||||||
|
@floor-switch-failed="handleFloorSwitchFailed"
|
||||||
@indoor-view-change="handleIndoorViewChange"
|
@indoor-view-change="handleIndoorViewChange"
|
||||||
@poi-click="handleGuidePoiClick"
|
@poi-click="handleGuidePoiClick"
|
||||||
@selection-clear="handleGuideSelectionClear"
|
@selection-clear="handleGuideSelectionClear"
|
||||||
@@ -291,7 +293,12 @@ const initialTopTab = (): GuideTopTab => topTabFromHash() || 'guide'
|
|||||||
const is3DMode = ref(true)
|
const is3DMode = ref(true)
|
||||||
const guideOutdoorState = ref<'home' | 'entrance'>('home')
|
const guideOutdoorState = ref<'home' | 'entrance'>('home')
|
||||||
const indoorView = ref<GuideIndoorView>('overview')
|
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)
|
const selectedGuidePoi = ref<GuideRenderPoi | null>(null)
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
@@ -330,6 +337,12 @@ const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
|
|||||||
const indoorModelSource = guideUseCase.getModelSource()
|
const indoorModelSource = guideUseCase.getModelSource()
|
||||||
const guideFloors = ref<MuseumFloor[]>([])
|
const guideFloors = ref<MuseumFloor[]>([])
|
||||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
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>(() => (
|
const indoorLayerMode = computed<LayerDisplayMode>(() => (
|
||||||
indoorView.value === 'multi' ? 'multi' : 'single'
|
indoorView.value === 'multi' ? 'multi' : 'single'
|
||||||
))
|
))
|
||||||
@@ -527,10 +540,14 @@ const loadGuideFloors = async () => {
|
|||||||
try {
|
try {
|
||||||
const floors = await guideUseCase.getFloors()
|
const floors = await guideUseCase.getFloors()
|
||||||
guideFloors.value = floors
|
guideFloors.value = floors
|
||||||
activeGuideFloor.value = floors.find((floor) => floor.label === activeGuideFloor.value)?.label
|
const normalizedActiveFloorId = activeGuideFloor.value
|
||||||
|| floors.find((floor) => floor.label === '1F')?.label
|
? normalizeGuideFloorId(activeGuideFloor.value)
|
||||||
|| floors[0]?.label
|
: ''
|
||||||
|
activeGuideFloor.value = floors.find((floor) => floor.id === normalizedActiveFloorId)?.id
|
||||||
|
|| floors.find((floor) => floor.label === '1F')?.id
|
||||||
|
|| floors[0]?.id
|
||||||
|| activeGuideFloor.value
|
|| activeGuideFloor.value
|
||||||
|
renderedFloorId.value = activeGuideFloor.value
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载导览楼层失败:', error)
|
console.error('加载导览楼层失败:', error)
|
||||||
guideFloors.value = []
|
guideFloors.value = []
|
||||||
@@ -632,13 +649,36 @@ const handleGuideSearchTap = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFloorChange = (floor: string) => {
|
const handleFloorRequest = ({ floorId, floorLabel }: { floorId: string; floorLabel: string }) => {
|
||||||
activeGuideFloor.value = floor
|
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'
|
indoorView.value = 'floor'
|
||||||
selectedGuidePoi.value = null
|
selectedGuidePoi.value = null
|
||||||
isPoiCardCollapsed.value = false
|
isPoiCardCollapsed.value = false
|
||||||
showIndoorHint(`已切换到 ${floor},单指旋转、双指可平移`, 3200)
|
showIndoorHint(`已切换到 ${getGuideFloorLabel(floorId)},单指旋转、双指可平移`, 3200)
|
||||||
console.log('切换楼层:', floor)
|
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) => {
|
const handleIndoorViewChange = (view: GuideIndoorView) => {
|
||||||
@@ -682,8 +722,8 @@ const handleGuidePoiClick = (poi: GuideRenderPoi) => {
|
|||||||
isPoiCardCollapsed.value = !selectedGuidePoiIsHall.value
|
isPoiCardCollapsed.value = !selectedGuidePoiIsHall.value
|
||||||
indoorView.value = 'floor'
|
indoorView.value = 'floor'
|
||||||
isSimulatingRoute.value = false
|
isSimulatingRoute.value = false
|
||||||
const matchedFloor = guideFloors.value.find((floor) => floor.id === poi.floorId)
|
activeGuideFloor.value = poi.floorId
|
||||||
activeGuideFloor.value = matchedFloor?.label || poi.floorId
|
renderedFloorId.value = poi.floorId
|
||||||
showIndoorHint('已在当前楼层标出点位', 3000)
|
showIndoorHint('已在当前楼层标出点位', 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,9 +927,9 @@ const handleRoutePickerSearch = ({ keyword }: { mode: 'start' | 'end'; keyword:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const focusRouteFloor = (route: GuideRouteResult) => {
|
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
|
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'
|
indoorView.value = routeFloorCount > 1 ? 'multi' : 'floor'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,7 +1015,13 @@ const guideStatusLabel = computed(() => {
|
|||||||
if (is3DMode.value) {
|
if (is3DMode.value) {
|
||||||
if (indoorView.value === 'overview') return '建筑外观'
|
if (indoorView.value === 'overview') return '建筑外观'
|
||||||
if (indoorView.value === 'multi') 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') {
|
if (guideOutdoorState.value === 'entrance') {
|
||||||
|
|||||||
@@ -126,6 +126,43 @@ const warnSgsGuideModelDiagnostics = (
|
|||||||
console.warn(`[SGS guide model] ${message}`, payload)
|
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) => (
|
const getSgsFloorModelMatchKeys = (floor: SgsSdkFloorSummaryPayload, label: string) => (
|
||||||
[
|
[
|
||||||
String(floor.floorId),
|
String(floor.floorId),
|
||||||
@@ -285,7 +322,10 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
|||||||
loadFloorData('navigablePlaces', () => this.provider.getNavigablePlaces(resolvedFloorId))
|
loadFloorData('navigablePlaces', () => this.provider.getNavigablePlaces(resolvedFloorId))
|
||||||
])
|
])
|
||||||
const ordinaryPois = pois.map((poi) => toSgsRenderPoi(toMuseumPoiFromSgs(poi, manifest.floors)))
|
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)
|
const hallDiagnostics = createSgsHallPoiDiagnostics(spaces, navigablePlaces, museumHallPois)
|
||||||
|
|
||||||
if (hallDiagnostics.eligibleSpaceCount > 0 && hallDiagnostics.hallPoiWithPositionCount === 0) {
|
if (hallDiagnostics.eligibleSpaceCount > 0 && hallDiagnostics.hallPoiWithPositionCount === 0) {
|
||||||
@@ -300,8 +340,21 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
|||||||
...hallPois,
|
...hallPois,
|
||||||
...ordinaryPois
|
...ordinaryPois
|
||||||
])
|
])
|
||||||
|
.filter((poi) => poi.floorId === resolvedFloorId)
|
||||||
.filter((poi) => Array.isArray(poi.positionGltf) && poi.positionGltf.length === 3)
|
.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')) {
|
if (hallDiagnostics.hallPoiWithPositionCount > 0 && !renderPois.some((poi) => poi.primaryCategory === 'exhibition_hall')) {
|
||||||
warnSgsGuideModelDiagnostics('render POI filter removed all hall POIs', {
|
warnSgsGuideModelDiagnostics('render POI filter removed all hall POIs', {
|
||||||
floorId: resolvedFloorId,
|
floorId: resolvedFloorId,
|
||||||
|
|||||||
Reference in New Issue
Block a user