修复馆内导览楼层点位展示
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user