@@ -117,6 +117,10 @@ const SGS_VISUAL_RENDER_CONFIG = {
|
||||
minPolarAngle: 0,
|
||||
maxPolarAngle: Math.PI / 2 - 0.05,
|
||||
dampingFactor: 0.06
|
||||
},
|
||||
framing: {
|
||||
overviewScreenOffsetRatio: new THREE.Vector2(-0.045, -0.095),
|
||||
floorScreenOffsetRatio: new THREE.Vector2(0, -0.055)
|
||||
}
|
||||
} as const
|
||||
|
||||
@@ -224,6 +228,9 @@ interface PoiMarkerCacheEntry {
|
||||
group: THREE.Group
|
||||
markerSize: number
|
||||
displayOffsetY: number
|
||||
rawPoiCount?: number
|
||||
rawPositionedPoiCount?: number
|
||||
filteredCategoryCounts?: Record<string, number>
|
||||
}
|
||||
|
||||
interface PoiYDistribution {
|
||||
@@ -374,6 +381,8 @@ let isProgrammaticCameraChange = false
|
||||
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let cameraTween: CameraTweenState | null = null
|
||||
let adjacentPreloadSeq = 0
|
||||
let firstModelLoadStartedAt = 0
|
||||
let firstModelVisibleReported = false
|
||||
|
||||
const cameraTweenEnabled = true
|
||||
const cameraTweenDurationMs = 480
|
||||
@@ -399,6 +408,79 @@ const floorExitAccumulatedZoomOutRatio = 0.26
|
||||
const floorExitCandidateHoldMs = 900
|
||||
const modelLoadRetryDelaysMs = [300, 900]
|
||||
const adjacentPreloadDelayMs = 900
|
||||
const poiMarkerVisualLiftFactor = 0.92
|
||||
const poiAmbientLabelLiftFactor = 2.1
|
||||
const poiFocusLabelLiftFactor = 2.72
|
||||
const poiFocusBaseLiftFactor = 0.18
|
||||
const poiFocusPulseLiftFactor = 0.34
|
||||
|
||||
type ThreeMapDiagnosticEvent =
|
||||
| 'init-start'
|
||||
| 'package-ready'
|
||||
| 'model-load-start'
|
||||
| 'model-load-complete'
|
||||
| 'model-load-failed'
|
||||
| 'first-model-visible'
|
||||
| 'poi-background-start'
|
||||
| 'poi-background-ready'
|
||||
| 'poi-filter-diagnostics'
|
||||
| 'poi-marker-render-diagnostics'
|
||||
| 'poi-hit-diagnostics'
|
||||
| 'adjacent-preload-skip'
|
||||
|
||||
const getNow = () => (
|
||||
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
||||
? performance.now()
|
||||
: Date.now()
|
||||
)
|
||||
|
||||
const isThreeMapDiagnosticsEnabled = () => {
|
||||
if (!import.meta.env.DEV) return false
|
||||
if (typeof window === 'undefined') return true
|
||||
|
||||
const diagnosticsWindow = window as unknown as {
|
||||
__GUIDE_3D_MODEL_DIAGNOSTICS__?: boolean
|
||||
}
|
||||
return diagnosticsWindow.__GUIDE_3D_MODEL_DIAGNOSTICS__ !== false
|
||||
}
|
||||
|
||||
const recordThreeMapDiagnostic = (
|
||||
event: ThreeMapDiagnosticEvent,
|
||||
payload: Record<string, unknown>
|
||||
) => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const diagnosticsWindow = window as unknown as {
|
||||
__GUIDE_3D_DIAGNOSTIC_EVENTS__?: Array<{
|
||||
source: string
|
||||
event: string
|
||||
payload: Record<string, unknown>
|
||||
}>
|
||||
}
|
||||
diagnosticsWindow.__GUIDE_3D_DIAGNOSTIC_EVENTS__ ||= []
|
||||
diagnosticsWindow.__GUIDE_3D_DIAGNOSTIC_EVENTS__.push({
|
||||
source: 'ThreeMap',
|
||||
event,
|
||||
payload
|
||||
})
|
||||
}
|
||||
|
||||
const stringifyDiagnosticPayload = (payload: Record<string, unknown>) => {
|
||||
try {
|
||||
return JSON.stringify(payload)
|
||||
} catch {
|
||||
return '[unserializable]'
|
||||
}
|
||||
}
|
||||
|
||||
const logThreeMapDiagnostic = (
|
||||
event: ThreeMapDiagnosticEvent,
|
||||
payload: Record<string, unknown> = {}
|
||||
) => {
|
||||
if (!isThreeMapDiagnosticsEnabled()) return
|
||||
recordThreeMapDiagnostic(event, payload)
|
||||
console.debug(`[ThreeMap] ${event} ${stringifyDiagnosticPayload(payload)}`)
|
||||
}
|
||||
|
||||
const syncControlInteractionOptions = () => {
|
||||
if (!controls) return
|
||||
@@ -411,6 +493,7 @@ const syncControlInteractionOptions = () => {
|
||||
}
|
||||
|
||||
const corePoiCategories = new Set([
|
||||
'poi',
|
||||
'touring_poi',
|
||||
'exhibition_hall',
|
||||
'exhibition_hall_entrance',
|
||||
@@ -424,6 +507,7 @@ const poiCategoryPriority: Record<string, number> = {
|
||||
exhibition_hall: 96,
|
||||
exhibition_hall_entrance: 92,
|
||||
touring_poi: 90,
|
||||
poi: 86,
|
||||
accessibility_special_service: 88,
|
||||
basic_service_facility: 82,
|
||||
transport_circulation: 76,
|
||||
@@ -444,6 +528,7 @@ const isServiceFacilityPoi = (poi: RenderPoi) => (
|
||||
const isTransportPoi = (poi: RenderPoi) => poi.primaryCategory === 'transport_circulation'
|
||||
|
||||
const poiGlyphMap: Record<string, string> = {
|
||||
poi: '点',
|
||||
accessible_restroom: '无',
|
||||
accessibility: '无',
|
||||
restroom: '卫',
|
||||
@@ -701,6 +786,52 @@ const getPoiDisplayMode = (): PoiDisplayMode => {
|
||||
|
||||
const getPoiMarkerCacheKey = (floorId: string, displayMode = getPoiDisplayMode()) => `${floorId}:${displayMode}`
|
||||
|
||||
const countPoisByCategory = (pois: RenderPoi[]) => pois.reduce<Record<string, number>>((counts, poi) => {
|
||||
const key = poi.primaryCategory || '<missing>'
|
||||
counts[key] = (counts[key] || 0) + 1
|
||||
return counts
|
||||
}, {})
|
||||
|
||||
const summarizeSelectedPoiForDiagnostics = (poi: RenderPoi | null) => (
|
||||
poi
|
||||
? {
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
floorId: poi.floorId,
|
||||
primaryCategory: poi.primaryCategory
|
||||
}
|
||||
: null
|
||||
)
|
||||
|
||||
const countPoiMarkerObjects = (group: THREE.Group) => {
|
||||
const counts = {
|
||||
markerCount: 0,
|
||||
hitTargetCount: 0,
|
||||
labelCount: 0
|
||||
}
|
||||
|
||||
group.traverse((child) => {
|
||||
if (!(child instanceof THREE.Sprite)) return
|
||||
|
||||
const userData = child.userData as PoiSpriteUserData
|
||||
if (userData.isPoiHitTarget) {
|
||||
counts.hitTargetCount += 1
|
||||
return
|
||||
}
|
||||
|
||||
if (userData.isPoiLabel) {
|
||||
counts.labelCount += 1
|
||||
return
|
||||
}
|
||||
|
||||
if (userData.poi) {
|
||||
counts.markerCount += 1
|
||||
}
|
||||
})
|
||||
|
||||
return counts
|
||||
}
|
||||
|
||||
const summarizePoiYDistribution = (pois: RenderPoi[]): PoiYDistribution | null => {
|
||||
const values = pois
|
||||
.map((poi) => poi.positionGltf?.[1])
|
||||
@@ -749,6 +880,24 @@ const getPoiDisplayPosition = (poi: RenderPoi, floorId = poi.floorId) => {
|
||||
return new THREE.Vector3(x, y + getCachedPoiDisplayOffsetY(floorId), z)
|
||||
}
|
||||
|
||||
const getPoiLiftedPosition = (
|
||||
displayPosition: THREE.Vector3,
|
||||
markerSize: number,
|
||||
liftFactor: number
|
||||
) => new THREE.Vector3(
|
||||
displayPosition.x,
|
||||
displayPosition.y + markerSize * liftFactor,
|
||||
displayPosition.z
|
||||
)
|
||||
|
||||
const getPoiMarkerPosition = (displayPosition: THREE.Vector3, markerSize: number) => (
|
||||
getPoiLiftedPosition(displayPosition, markerSize, poiMarkerVisualLiftFactor)
|
||||
)
|
||||
|
||||
const getPoiAmbientLabelPosition = (displayPosition: THREE.Vector3, markerSize: number) => (
|
||||
getPoiLiftedPosition(displayPosition, markerSize, poiAmbientLabelLiftFactor)
|
||||
)
|
||||
|
||||
const resolvePoiDisplayOffsetY = (
|
||||
floorId: string,
|
||||
model: THREE.Object3D,
|
||||
@@ -1037,7 +1186,7 @@ const updateAmbientPoiLabels = () => {
|
||||
|
||||
updateAmbientLabelScale(labelSprite, poi)
|
||||
|
||||
const baseOffset = markerSize * (isHallPoi(poi) ? 1.82 : 1.5)
|
||||
const baseOffset = markerSize * (isHallPoi(poi) ? poiAmbientLabelLiftFactor : 1.82)
|
||||
const offsetAttempts = [
|
||||
0,
|
||||
markerSize * 0.82,
|
||||
@@ -1295,6 +1444,23 @@ const setProgress = (progress: number, message: string) => {
|
||||
loadingMessage.value = message
|
||||
}
|
||||
|
||||
const markFirstModelVisible = (
|
||||
view: ViewMode,
|
||||
payload: Record<string, unknown> = {}
|
||||
) => {
|
||||
if (firstModelVisibleReported) return
|
||||
firstModelVisibleReported = true
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
logThreeMapDiagnostic('first-model-visible', {
|
||||
view,
|
||||
elapsedMs: firstModelLoadStartedAt ? Math.round(getNow() - firstModelLoadStartedAt) : undefined,
|
||||
diagnostics: guideModelLoadManager.getDiagnostics(),
|
||||
...payload
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const staleModelLoadMessage = 'STALE_MODEL_LOAD'
|
||||
|
||||
const startModelLoad = () => {
|
||||
@@ -2303,20 +2469,46 @@ const loadModelOnce = (
|
||||
return
|
||||
}
|
||||
|
||||
const startedAt = getNow()
|
||||
logThreeMapDiagnostic('model-load-start', {
|
||||
label,
|
||||
url
|
||||
})
|
||||
|
||||
loader.load(
|
||||
url,
|
||||
resolve,
|
||||
(gltf) => {
|
||||
logThreeMapDiagnostic('model-load-complete', {
|
||||
label,
|
||||
url,
|
||||
elapsedMs: Math.round(getNow() - startedAt)
|
||||
})
|
||||
resolve(gltf)
|
||||
},
|
||||
(event) => {
|
||||
if (options.suppressProgress || (loadToken !== undefined && !isCurrentModelLoad(loadToken))) return
|
||||
|
||||
if (event.total > 0) {
|
||||
const modelProgress = Math.round((event.loaded / event.total) * 70)
|
||||
setProgress(20 + modelProgress, `${label}: ${Math.round((event.loaded / event.total) * 100)}%`)
|
||||
const loadedRatio = event.loaded / event.total
|
||||
const percent = Math.round(loadedRatio * 100)
|
||||
const modelProgress = Math.round(Math.min(loadedRatio, 0.98) * 68)
|
||||
const progressMessage = percent >= 98
|
||||
? `${label}: 下载完成,正在解析模型...`
|
||||
: `${label}: ${percent}%`
|
||||
setProgress(20 + modelProgress, progressMessage)
|
||||
} else {
|
||||
setProgress(45, label)
|
||||
setProgress(45, `${label}: 正在下载模型...`)
|
||||
}
|
||||
},
|
||||
reject
|
||||
(error) => {
|
||||
logThreeMapDiagnostic('model-load-failed', {
|
||||
label,
|
||||
url,
|
||||
elapsedMs: Math.round(getNow() - startedAt),
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
reject(error)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2377,6 +2569,9 @@ const loadModelWithFallback = async (
|
||||
loadSource: (url) => loadModelFromNetwork(url, label, loadToken, options),
|
||||
shouldStopOnError: isStaleModelLoadError
|
||||
})
|
||||
if (!options.suppressProgress) {
|
||||
setProgress(92, `${label}: 正在准备三维场景...`)
|
||||
}
|
||||
|
||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
||||
disposeObject(gltf.scene)
|
||||
@@ -2460,15 +2655,32 @@ const preloadModelUrls = async (urls: string[], label: string, preloadSeq: numbe
|
||||
const candidates = urls.map((url) => url.trim()).filter(Boolean)
|
||||
if (!candidates.length || preloadSeq !== adjacentPreloadSeq || isDisposed) return
|
||||
|
||||
const availableCandidates = candidates.filter((url) => {
|
||||
const isCached = guideModelLoadManager.has(url)
|
||||
const isPending = guideModelLoadManager.isInFlight(url)
|
||||
if (isCached || isPending) {
|
||||
logThreeMapDiagnostic('adjacent-preload-skip', {
|
||||
label,
|
||||
url,
|
||||
reason: isCached ? 'cache-hit' : 'in-flight-hit',
|
||||
diagnostics: guideModelLoadManager.getDiagnostics()
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
if (!availableCandidates.length) return
|
||||
|
||||
try {
|
||||
await guideModelLoadManager.preload({
|
||||
urls: candidates,
|
||||
urls: availableCandidates,
|
||||
label,
|
||||
loadSource: (url) => loadModelFromNetwork(url, label, undefined, { suppressProgress: true })
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('[ThreeMap] 相邻楼层模型预加载失败:', {
|
||||
urls: candidates,
|
||||
urls: availableCandidates,
|
||||
label,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
@@ -2497,10 +2709,25 @@ const scheduleAdjacentFloorPreload = (floorId: string) => {
|
||||
if (preloadSeq !== adjacentPreloadSeq || isDisposed) return
|
||||
|
||||
void (async () => {
|
||||
const seenModelKeys = new Set<string>()
|
||||
for (const floor of adjacentFloors) {
|
||||
if (preloadSeq !== adjacentPreloadSeq || isDisposed) return
|
||||
const uniqueUrls = getFloorModelUrls(floor).filter((url) => {
|
||||
const key = guideModelLoadManager.createCacheKey(url)
|
||||
if (!key) return false
|
||||
if (seenModelKeys.has(key)) {
|
||||
logThreeMapDiagnostic('adjacent-preload-skip', {
|
||||
floorId: floor.floorId,
|
||||
url,
|
||||
reason: 'duplicate-shared-url'
|
||||
})
|
||||
return false
|
||||
}
|
||||
seenModelKeys.add(key)
|
||||
return true
|
||||
})
|
||||
await preloadModelUrls(
|
||||
getFloorModelUrls(floor),
|
||||
uniqueUrls,
|
||||
`预加载 ${formatFloorLabel(floor.floorId)} 模型`,
|
||||
preloadSeq
|
||||
)
|
||||
@@ -2980,12 +3207,13 @@ const fitCameraToObject = (object: THREE.Object3D, preset: CameraPreset = 'obliq
|
||||
const overviewFitOptions: CameraFitOptions = activeView.value === 'overview'
|
||||
? {
|
||||
distanceFactor: 0.9,
|
||||
screenOffsetRatio: new THREE.Vector2(-0.045, 0.02),
|
||||
screenOffsetRatio: SGS_VISUAL_RENDER_CONFIG.framing.overviewScreenOffsetRatio,
|
||||
up: new THREE.Vector3(0, 1, 0)
|
||||
}
|
||||
: activeView.value === 'floor'
|
||||
? {
|
||||
distanceFactor: FLOOR_CAMERA_DISTANCE_FACTOR,
|
||||
screenOffsetRatio: SGS_VISUAL_RENDER_CONFIG.framing.floorScreenOffsetRatio,
|
||||
up: new THREE.Vector3(0, 1, 0)
|
||||
}
|
||||
: {}
|
||||
@@ -3069,9 +3297,20 @@ const loadCurrentFloorPoiMarkers = async (loadToken: number) => {
|
||||
const loadCurrentFloorPoiMarkersInBackground = (loadToken: number) => {
|
||||
if (!shouldRenderPoiMarkers.value) return
|
||||
|
||||
const startedAt = getNow()
|
||||
logThreeMapDiagnostic('poi-background-start', {
|
||||
floorId: currentFloor.value,
|
||||
view: activeView.value
|
||||
})
|
||||
|
||||
void loadCurrentFloorPoiMarkers(loadToken)
|
||||
.then(() => {
|
||||
if (!isCurrentModelLoad(loadToken)) return
|
||||
logThreeMapDiagnostic('poi-background-ready', {
|
||||
floorId: currentFloor.value,
|
||||
view: activeView.value,
|
||||
elapsedMs: Math.round(getNow() - startedAt)
|
||||
})
|
||||
refreshPoiVisibilityByDistance()
|
||||
renderRoutePreview()
|
||||
})
|
||||
@@ -3103,6 +3342,10 @@ const loadOverview = async () => {
|
||||
resetAutoSwitchDistanceTracking()
|
||||
refreshPoiVisibilityByDistance()
|
||||
renderRoutePreview()
|
||||
markFirstModelVisible('overview', {
|
||||
source: 'cached-overview',
|
||||
modelUrl: cachedSharedModelUrl
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3130,6 +3373,10 @@ const loadOverview = async () => {
|
||||
resetAutoSwitchDistanceTracking()
|
||||
refreshPoiVisibilityByDistance()
|
||||
renderRoutePreview()
|
||||
markFirstModelVisible('overview', {
|
||||
source: 'network',
|
||||
modelUrl: packageData.overviewModelUrl
|
||||
})
|
||||
}
|
||||
|
||||
const prepareFloorScene = async (
|
||||
@@ -3190,17 +3437,9 @@ const prepareFloorScene = async (
|
||||
throw new Error(`楼层模型加载失败:${requestedFloorId}`)
|
||||
}
|
||||
|
||||
let poiEntry: PoiMarkerCacheEntry | undefined | null = null
|
||||
try {
|
||||
poiEntry = shouldRenderPoiMarkers.value
|
||||
? await prepareFloorPOIs(floor, loadToken, model)
|
||||
: null
|
||||
} catch (error) {
|
||||
if (ownsModel) {
|
||||
disposeObject(model, protectedResources)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
const poiEntry = shouldRenderPoiMarkers.value
|
||||
? poiMarkerGroupCache.get(getPoiMarkerCacheKey(floor.floorId)) || null
|
||||
: null
|
||||
|
||||
const prepared: PreparedFloorScene = {
|
||||
floor,
|
||||
@@ -3260,6 +3499,12 @@ const commitPreparedFloorScene = (
|
||||
markFloorAutoSwitchEntry()
|
||||
refreshPoiVisibilityByDistance()
|
||||
renderRoutePreview()
|
||||
markFirstModelVisible('floor', {
|
||||
floorId: expectedFloorId,
|
||||
modelUrl: prepared.floor.modelUrl,
|
||||
poiFromCache: Boolean(prepared.poiEntry)
|
||||
})
|
||||
loadCurrentFloorPoiMarkersInBackground(loadToken)
|
||||
scheduleAdjacentFloorPreload(expectedFloorId)
|
||||
}
|
||||
|
||||
@@ -3370,6 +3615,11 @@ const loadMultiFloor = async () => {
|
||||
resetAutoSwitchDistanceTracking()
|
||||
refreshPoiVisibilityByDistance()
|
||||
renderRoutePreview()
|
||||
markFirstModelVisible('multi', {
|
||||
source: 'shared-model',
|
||||
modelUrl: sharedModelUrl,
|
||||
floorCount: orderedFloors.length
|
||||
})
|
||||
return
|
||||
} catch (error) {
|
||||
disposeObject(group, protectedResources)
|
||||
@@ -3441,6 +3691,10 @@ const loadMultiFloor = async () => {
|
||||
resetAutoSwitchDistanceTracking()
|
||||
refreshPoiVisibilityByDistance()
|
||||
renderRoutePreview()
|
||||
markFirstModelVisible('multi', {
|
||||
source: 'per-floor-models',
|
||||
floorCount: orderedFloors.length
|
||||
})
|
||||
}
|
||||
|
||||
const createPoiMaterial = (poi: RenderPoi) => {
|
||||
@@ -3586,6 +3840,7 @@ const createPoiLabelSprite = (poi: RenderPoi, markerSize: number) => {
|
||||
sprite.userData.labelBaseScaleX = markerSize * 5.6
|
||||
sprite.userData.labelBaseScaleY = markerSize * 1.68
|
||||
sprite.renderOrder = 30
|
||||
sprite.frustumCulled = false
|
||||
sprite.scale.set(sprite.userData.labelBaseScaleX, sprite.userData.labelBaseScaleY, 1)
|
||||
|
||||
return sprite
|
||||
@@ -3661,6 +3916,7 @@ const createAmbientPoiLabelSprite = (poi: RenderPoi, markerSize: number) => {
|
||||
sprite.userData.labelBaseScaleX = markerSize * (isHallPoi(poi) ? 3.9 : 3.05)
|
||||
sprite.userData.labelBaseScaleY = markerSize * (isHallPoi(poi) ? 1.22 : 0.95)
|
||||
sprite.renderOrder = isHallPoi(poi) ? 18 : 15
|
||||
sprite.frustumCulled = false
|
||||
sprite.scale.set(sprite.userData.labelBaseScaleX, sprite.userData.labelBaseScaleY, 1)
|
||||
sprite.visible = false
|
||||
|
||||
@@ -3697,6 +3953,7 @@ const createPoiPulseSprite = (poi: RenderPoi, markerSize: number) => {
|
||||
sprite.userData.isPoiPulse = true
|
||||
sprite.userData.baseScale = markerSize * 1.9
|
||||
sprite.renderOrder = 9
|
||||
sprite.frustumCulled = false
|
||||
sprite.scale.set(markerSize * 1.9, markerSize * 1.9, markerSize * 1.9)
|
||||
|
||||
return sprite
|
||||
@@ -3734,6 +3991,7 @@ const createPoiBaseSprite = (poi: RenderPoi, markerSize: number) => {
|
||||
sprite.userData.isPoiBase = true
|
||||
sprite.userData.baseScale = markerSize * 2.15
|
||||
sprite.renderOrder = 8
|
||||
sprite.frustumCulled = false
|
||||
sprite.scale.set(markerSize * 2.15, markerSize * 2.15, markerSize * 2.15)
|
||||
|
||||
return sprite
|
||||
@@ -3771,7 +4029,8 @@ const showFocusPoiLabel = (poi: RenderPoi) => {
|
||||
|
||||
const markerSize = getPoiMarkerSize()
|
||||
activeFocusLabelSprite = createPoiLabelSprite(poi, markerSize)
|
||||
activeFocusLabelSprite.position.set(displayPosition.x, displayPosition.y + markerSize * 2.35, displayPosition.z)
|
||||
activeFocusLabelSprite.frustumCulled = false
|
||||
activeFocusLabelSprite.position.copy(getPoiLiftedPosition(displayPosition, markerSize, poiFocusLabelLiftFactor))
|
||||
updateFocusLabelScale()
|
||||
poiGroup.add(activeFocusLabelSprite)
|
||||
}
|
||||
@@ -3784,7 +4043,8 @@ const showFocusPoiBase = (poi: RenderPoi) => {
|
||||
|
||||
const markerSize = getPoiMarkerSize()
|
||||
activeFocusBaseSprite = createPoiBaseSprite(poi, markerSize)
|
||||
activeFocusBaseSprite.position.set(displayPosition.x, displayPosition.y + markerSize * 0.04, displayPosition.z)
|
||||
activeFocusBaseSprite.frustumCulled = false
|
||||
activeFocusBaseSprite.position.copy(getPoiLiftedPosition(displayPosition, markerSize, poiFocusBaseLiftFactor))
|
||||
poiGroup.add(activeFocusBaseSprite)
|
||||
}
|
||||
|
||||
@@ -3796,7 +4056,8 @@ const showFocusPoiPulse = (poi: RenderPoi) => {
|
||||
|
||||
const markerSize = getPoiMarkerSize()
|
||||
activeFocusPulseSprite = createPoiPulseSprite(poi, markerSize)
|
||||
activeFocusPulseSprite.position.set(displayPosition.x, displayPosition.y + markerSize * 0.15, displayPosition.z)
|
||||
activeFocusPulseSprite.frustumCulled = false
|
||||
activeFocusPulseSprite.position.copy(getPoiLiftedPosition(displayPosition, markerSize, poiFocusPulseLiftFactor))
|
||||
poiGroup.add(activeFocusPulseSprite)
|
||||
}
|
||||
|
||||
@@ -3809,6 +4070,7 @@ const showFocusPoiAffordances = (poi: RenderPoi) => {
|
||||
|
||||
const getPoiColor = (category: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
poi: '#56606b',
|
||||
touring_poi: '#2f6fed',
|
||||
exhibition_hall: '#2f6fed',
|
||||
exhibition_hall_entrance: '#4659d8',
|
||||
@@ -3900,6 +4162,7 @@ const createPoiMarkerSprites = (poi: RenderPoi, markerSize: number) => {
|
||||
sprite.userData.poi = poi
|
||||
sprite.userData.isCorePoi = isCorePoi
|
||||
sprite.userData.hitTarget = hitTarget
|
||||
sprite.frustumCulled = false
|
||||
if (labelSprite) {
|
||||
sprite.userData.labelSprite = labelSprite
|
||||
}
|
||||
@@ -3910,6 +4173,7 @@ const createPoiMarkerSprites = (poi: RenderPoi, markerSize: number) => {
|
||||
hitTarget.userData.visibleMarker = sprite
|
||||
hitTarget.userData.isPoiHitTarget = true
|
||||
hitTarget.renderOrder = 11
|
||||
hitTarget.frustumCulled = false
|
||||
hitTarget.scale.set(hitTargetScale, hitTargetScale, hitTargetScale)
|
||||
|
||||
setPoiSpriteFocusStyle(sprite, poi.id === activeFocusPoiId.value)
|
||||
@@ -3941,7 +4205,7 @@ const addFocusMarkerFromRequest = (request: TargetPoiFocusRequest) => {
|
||||
|
||||
const markerSize = getPoiMarkerSize()
|
||||
const { sprite, hitTarget } = createPoiMarkerSprites(poi, markerSize)
|
||||
sprite.position.set(displayPosition.x, displayPosition.y + markerSize * 0.5, displayPosition.z)
|
||||
sprite.position.copy(getPoiMarkerPosition(displayPosition, markerSize))
|
||||
hitTarget.position.copy(sprite.position)
|
||||
poiGroup.add(sprite)
|
||||
poiGroup.add(hitTarget)
|
||||
@@ -3966,10 +4230,10 @@ const createPoiMarkerGroup = (
|
||||
if (!displayPosition) return
|
||||
|
||||
const { sprite, hitTarget, labelSprite } = createPoiMarkerSprites(poi, markerSize)
|
||||
sprite.position.set(displayPosition.x, displayPosition.y + markerSize * 0.5, displayPosition.z)
|
||||
sprite.position.copy(getPoiMarkerPosition(displayPosition, markerSize))
|
||||
hitTarget.position.copy(sprite.position)
|
||||
if (labelSprite) {
|
||||
labelSprite.position.set(displayPosition.x, displayPosition.y + markerSize * 1.82, displayPosition.z)
|
||||
labelSprite.position.copy(getPoiAmbientLabelPosition(displayPosition, markerSize))
|
||||
labelSprite.visible = false
|
||||
}
|
||||
sprite.visible = shouldShowPoiAtDistance(poi, getPoiVisibilityTier())
|
||||
@@ -3994,6 +4258,24 @@ const attachPoiMarkerGroup = (entry: PoiMarkerCacheEntry) => {
|
||||
poiGroup.userData.floorId = entry.floorId
|
||||
poiGroup.userData.currentFloor = entry.floorId
|
||||
updatePoiMarkerFocus()
|
||||
logThreeMapDiagnostic('poi-marker-render-diagnostics', {
|
||||
floorId: entry.floorId,
|
||||
currentFloor: currentFloor.value,
|
||||
displayMode: entry.displayMode,
|
||||
rawPoiCount: entry.rawPoiCount ?? entry.pois.length,
|
||||
rawPositionedPoiCount: entry.rawPositionedPoiCount ?? entry.pois.filter((poi) => Boolean(poi.positionGltf)).length,
|
||||
renderPoiCount: entry.pois.length,
|
||||
renderPositionedPoiCount: entry.pois.filter((poi) => Boolean(poi.positionGltf)).length,
|
||||
renderCategoryCounts: countPoisByCategory(entry.pois),
|
||||
filteredCategoryCounts: entry.filteredCategoryCounts || {},
|
||||
markerObjects: countPoiMarkerObjects(entry.group),
|
||||
poiGroupFloorId: poiGroup.userData.floorId,
|
||||
poiGroupCurrentFloor: poiGroup.userData.currentFloor,
|
||||
selectedPoi: summarizeSelectedPoiForDiagnostics(selectedPOI.value),
|
||||
markerSize: roundDiagnosticNumber(entry.markerSize),
|
||||
displayOffsetY: roundDiagnosticNumber(entry.displayOffsetY),
|
||||
visualLiftY: roundDiagnosticNumber(entry.markerSize * poiMarkerVisualLiftFactor)
|
||||
})
|
||||
}
|
||||
|
||||
const restoreCommittedFloorPoiLayer = () => {
|
||||
@@ -4044,6 +4326,21 @@ const prepareFloorPOIs = async (
|
||||
|
||||
poiDataCache.set(floor.floorId, floorPois)
|
||||
const validPois = floorPois.filter((poi) => shouldShowPoiInCurrentMode(poi))
|
||||
const filteredPois = floorPois.filter((poi) => !validPois.some((validPoi) => validPoi.id === poi.id))
|
||||
logThreeMapDiagnostic('poi-filter-diagnostics', {
|
||||
floorId: floor.floorId,
|
||||
currentFloor: currentFloor.value,
|
||||
displayMode,
|
||||
rawPoiCount: floorPois.length,
|
||||
rawPositionedPoiCount: floorPois.filter((poi) => Boolean(poi.positionGltf)).length,
|
||||
rawCategoryCounts: countPoisByCategory(floorPois),
|
||||
poiCategoryCount: floorPois.filter((poi) => poi.primaryCategory === 'poi').length,
|
||||
renderPoiCount: validPois.length,
|
||||
renderPositionedPoiCount: validPois.filter((poi) => Boolean(poi.positionGltf)).length,
|
||||
renderCategoryCounts: countPoisByCategory(validPois),
|
||||
renderPoiCategoryCount: validPois.filter((poi) => poi.primaryCategory === 'poi').length,
|
||||
filteredCategoryCounts: countPoisByCategory(filteredPois)
|
||||
})
|
||||
const displayOffsetY = markerModel
|
||||
? resolvePoiDisplayOffsetY(floor.floorId, markerModel, validPois)
|
||||
: getCachedPoiDisplayOffsetY(floor.floorId)
|
||||
@@ -4055,7 +4352,10 @@ const prepareFloorPOIs = async (
|
||||
pois: validPois,
|
||||
group: markerGroup,
|
||||
markerSize,
|
||||
displayOffsetY
|
||||
displayOffsetY,
|
||||
rawPoiCount: floorPois.length,
|
||||
rawPositionedPoiCount: floorPois.filter((poi) => Boolean(poi.positionGltf)).length,
|
||||
filteredCategoryCounts: countPoisByCategory(filteredPois)
|
||||
}
|
||||
|
||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
||||
@@ -4140,13 +4440,31 @@ const handleSceneTap = async (event: PointerEvent) => {
|
||||
raycaster.setFromCamera(pointer, camera)
|
||||
|
||||
if ((activeView.value === 'floor' || activeView.value === 'overview') && poiGroup) {
|
||||
const poiHits = raycaster.intersectObjects([...getPoiHitTargets(), ...getPoiSprites()], false)
|
||||
const hitTargets = getPoiHitTargets()
|
||||
const poiSprites = getPoiSprites()
|
||||
const poiHits = raycaster.intersectObjects([...hitTargets, ...poiSprites], false)
|
||||
const hit = poiHits.find((item) => (
|
||||
item.object.visible
|
||||
&& item.object instanceof THREE.Sprite
|
||||
&& getPoiMarkerFromHit(item.object)
|
||||
))
|
||||
const hitMarker = hit ? getPoiMarkerFromHit(hit.object) : findNearestPoiMarkerByScreenPoint(event, rect)
|
||||
const fallbackMarker = hit ? null : findNearestPoiMarkerByScreenPoint(event, rect)
|
||||
const hitMarker = hit ? getPoiMarkerFromHit(hit.object) : fallbackMarker
|
||||
|
||||
logThreeMapDiagnostic('poi-hit-diagnostics', {
|
||||
view: activeView.value,
|
||||
currentFloor: currentFloor.value,
|
||||
poiGroupFloorId: poiGroup.userData.floorId,
|
||||
pointer: {
|
||||
x: Math.round(event.clientX - rect.left),
|
||||
y: Math.round(event.clientY - rect.top)
|
||||
},
|
||||
visibleMarkerCount: poiSprites.filter((sprite) => sprite.visible).length,
|
||||
hitTargetCount: hitTargets.length,
|
||||
rayHitCount: poiHits.length,
|
||||
raySelectedPoiId: hit ? (getPoiMarkerFromHit(hit.object)?.userData.poi as RenderPoi | undefined)?.id : null,
|
||||
fallbackSelectedPoiId: (fallbackMarker?.userData.poi as RenderPoi | undefined)?.id || null
|
||||
})
|
||||
|
||||
if (hitMarker?.userData.poi) {
|
||||
const selectedPoi = hitMarker.userData.poi as RenderPoi
|
||||
@@ -4352,6 +4670,17 @@ const loadModelPackage = async () => {
|
||||
|
||||
setProgress(14, '正在读取楼层索引...')
|
||||
floorIndex.value = [...packageData.floors].sort(compareFloorsTopToBottom)
|
||||
logThreeMapDiagnostic('package-ready', {
|
||||
initialView: props.initialView,
|
||||
requestedInitialFloorId: props.initialFloorId,
|
||||
overviewModelUrl: packageData.overviewModelUrl,
|
||||
floorCount: floorIndex.value.length,
|
||||
floorModels: floorIndex.value.map((floor) => ({
|
||||
floorId: floor.floorId,
|
||||
modelUrl: floor.modelUrl,
|
||||
sharedModelAsset: floor.sharedModelAsset
|
||||
}))
|
||||
})
|
||||
|
||||
const requestedInitialFloorId = resolveFloorIdFromRequest(props.initialFloorId)
|
||||
if (requestedInitialFloorId) {
|
||||
@@ -4380,11 +4709,17 @@ const init3DScene = async () => {
|
||||
try {
|
||||
disposeScene()
|
||||
isDisposed = false
|
||||
firstModelLoadStartedAt = getNow()
|
||||
firstModelVisibleReported = false
|
||||
isLoading.value = true
|
||||
loadError.value = false
|
||||
selectedPOI.value = null
|
||||
hasLoadedFloorViewOnce = false
|
||||
setProgress(0, '正在初始化三维场景...')
|
||||
logThreeMapDiagnostic('init-start', {
|
||||
initialView: props.initialView,
|
||||
initialFloorId: props.initialFloorId
|
||||
})
|
||||
|
||||
await initThree()
|
||||
await loadModelPackage()
|
||||
@@ -4780,7 +5115,8 @@ onUnmounted(() => {
|
||||
border: 1px solid rgba(31, 35, 41, 0.1);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 28px rgba(31, 35, 41, 0.16);
|
||||
z-index: 14;
|
||||
z-index: 60;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.poi-content {
|
||||
|
||||
Reference in New Issue
Block a user