@@ -117,6 +117,10 @@ const SGS_VISUAL_RENDER_CONFIG = {
|
|||||||
minPolarAngle: 0,
|
minPolarAngle: 0,
|
||||||
maxPolarAngle: Math.PI / 2 - 0.05,
|
maxPolarAngle: Math.PI / 2 - 0.05,
|
||||||
dampingFactor: 0.06
|
dampingFactor: 0.06
|
||||||
|
},
|
||||||
|
framing: {
|
||||||
|
overviewScreenOffsetRatio: new THREE.Vector2(-0.045, -0.095),
|
||||||
|
floorScreenOffsetRatio: new THREE.Vector2(0, -0.055)
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -224,6 +228,9 @@ interface PoiMarkerCacheEntry {
|
|||||||
group: THREE.Group
|
group: THREE.Group
|
||||||
markerSize: number
|
markerSize: number
|
||||||
displayOffsetY: number
|
displayOffsetY: number
|
||||||
|
rawPoiCount?: number
|
||||||
|
rawPositionedPoiCount?: number
|
||||||
|
filteredCategoryCounts?: Record<string, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PoiYDistribution {
|
interface PoiYDistribution {
|
||||||
@@ -374,6 +381,8 @@ let isProgrammaticCameraChange = false
|
|||||||
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
|
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let cameraTween: CameraTweenState | null = null
|
let cameraTween: CameraTweenState | null = null
|
||||||
let adjacentPreloadSeq = 0
|
let adjacentPreloadSeq = 0
|
||||||
|
let firstModelLoadStartedAt = 0
|
||||||
|
let firstModelVisibleReported = false
|
||||||
|
|
||||||
const cameraTweenEnabled = true
|
const cameraTweenEnabled = true
|
||||||
const cameraTweenDurationMs = 480
|
const cameraTweenDurationMs = 480
|
||||||
@@ -399,6 +408,79 @@ const floorExitAccumulatedZoomOutRatio = 0.26
|
|||||||
const floorExitCandidateHoldMs = 900
|
const floorExitCandidateHoldMs = 900
|
||||||
const modelLoadRetryDelaysMs = [300, 900]
|
const modelLoadRetryDelaysMs = [300, 900]
|
||||||
const adjacentPreloadDelayMs = 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 = () => {
|
const syncControlInteractionOptions = () => {
|
||||||
if (!controls) return
|
if (!controls) return
|
||||||
@@ -411,6 +493,7 @@ const syncControlInteractionOptions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const corePoiCategories = new Set([
|
const corePoiCategories = new Set([
|
||||||
|
'poi',
|
||||||
'touring_poi',
|
'touring_poi',
|
||||||
'exhibition_hall',
|
'exhibition_hall',
|
||||||
'exhibition_hall_entrance',
|
'exhibition_hall_entrance',
|
||||||
@@ -424,6 +507,7 @@ const poiCategoryPriority: Record<string, number> = {
|
|||||||
exhibition_hall: 96,
|
exhibition_hall: 96,
|
||||||
exhibition_hall_entrance: 92,
|
exhibition_hall_entrance: 92,
|
||||||
touring_poi: 90,
|
touring_poi: 90,
|
||||||
|
poi: 86,
|
||||||
accessibility_special_service: 88,
|
accessibility_special_service: 88,
|
||||||
basic_service_facility: 82,
|
basic_service_facility: 82,
|
||||||
transport_circulation: 76,
|
transport_circulation: 76,
|
||||||
@@ -444,6 +528,7 @@ const isServiceFacilityPoi = (poi: RenderPoi) => (
|
|||||||
const isTransportPoi = (poi: RenderPoi) => poi.primaryCategory === 'transport_circulation'
|
const isTransportPoi = (poi: RenderPoi) => poi.primaryCategory === 'transport_circulation'
|
||||||
|
|
||||||
const poiGlyphMap: Record<string, string> = {
|
const poiGlyphMap: Record<string, string> = {
|
||||||
|
poi: '点',
|
||||||
accessible_restroom: '无',
|
accessible_restroom: '无',
|
||||||
accessibility: '无',
|
accessibility: '无',
|
||||||
restroom: '卫',
|
restroom: '卫',
|
||||||
@@ -701,6 +786,52 @@ const getPoiDisplayMode = (): PoiDisplayMode => {
|
|||||||
|
|
||||||
const getPoiMarkerCacheKey = (floorId: string, displayMode = getPoiDisplayMode()) => `${floorId}:${displayMode}`
|
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 summarizePoiYDistribution = (pois: RenderPoi[]): PoiYDistribution | null => {
|
||||||
const values = pois
|
const values = pois
|
||||||
.map((poi) => poi.positionGltf?.[1])
|
.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)
|
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 = (
|
const resolvePoiDisplayOffsetY = (
|
||||||
floorId: string,
|
floorId: string,
|
||||||
model: THREE.Object3D,
|
model: THREE.Object3D,
|
||||||
@@ -1037,7 +1186,7 @@ const updateAmbientPoiLabels = () => {
|
|||||||
|
|
||||||
updateAmbientLabelScale(labelSprite, poi)
|
updateAmbientLabelScale(labelSprite, poi)
|
||||||
|
|
||||||
const baseOffset = markerSize * (isHallPoi(poi) ? 1.82 : 1.5)
|
const baseOffset = markerSize * (isHallPoi(poi) ? poiAmbientLabelLiftFactor : 1.82)
|
||||||
const offsetAttempts = [
|
const offsetAttempts = [
|
||||||
0,
|
0,
|
||||||
markerSize * 0.82,
|
markerSize * 0.82,
|
||||||
@@ -1295,6 +1444,23 @@ const setProgress = (progress: number, message: string) => {
|
|||||||
loadingMessage.value = message
|
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 staleModelLoadMessage = 'STALE_MODEL_LOAD'
|
||||||
|
|
||||||
const startModelLoad = () => {
|
const startModelLoad = () => {
|
||||||
@@ -2303,20 +2469,46 @@ const loadModelOnce = (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startedAt = getNow()
|
||||||
|
logThreeMapDiagnostic('model-load-start', {
|
||||||
|
label,
|
||||||
|
url
|
||||||
|
})
|
||||||
|
|
||||||
loader.load(
|
loader.load(
|
||||||
url,
|
url,
|
||||||
resolve,
|
(gltf) => {
|
||||||
|
logThreeMapDiagnostic('model-load-complete', {
|
||||||
|
label,
|
||||||
|
url,
|
||||||
|
elapsedMs: Math.round(getNow() - startedAt)
|
||||||
|
})
|
||||||
|
resolve(gltf)
|
||||||
|
},
|
||||||
(event) => {
|
(event) => {
|
||||||
if (options.suppressProgress || (loadToken !== undefined && !isCurrentModelLoad(loadToken))) return
|
if (options.suppressProgress || (loadToken !== undefined && !isCurrentModelLoad(loadToken))) return
|
||||||
|
|
||||||
if (event.total > 0) {
|
if (event.total > 0) {
|
||||||
const modelProgress = Math.round((event.loaded / event.total) * 70)
|
const loadedRatio = event.loaded / event.total
|
||||||
setProgress(20 + modelProgress, `${label}: ${Math.round((event.loaded / event.total) * 100)}%`)
|
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 {
|
} 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),
|
loadSource: (url) => loadModelFromNetwork(url, label, loadToken, options),
|
||||||
shouldStopOnError: isStaleModelLoadError
|
shouldStopOnError: isStaleModelLoadError
|
||||||
})
|
})
|
||||||
|
if (!options.suppressProgress) {
|
||||||
|
setProgress(92, `${label}: 正在准备三维场景...`)
|
||||||
|
}
|
||||||
|
|
||||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
||||||
disposeObject(gltf.scene)
|
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)
|
const candidates = urls.map((url) => url.trim()).filter(Boolean)
|
||||||
if (!candidates.length || preloadSeq !== adjacentPreloadSeq || isDisposed) return
|
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 {
|
try {
|
||||||
await guideModelLoadManager.preload({
|
await guideModelLoadManager.preload({
|
||||||
urls: candidates,
|
urls: availableCandidates,
|
||||||
label,
|
label,
|
||||||
loadSource: (url) => loadModelFromNetwork(url, label, undefined, { suppressProgress: true })
|
loadSource: (url) => loadModelFromNetwork(url, label, undefined, { suppressProgress: true })
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[ThreeMap] 相邻楼层模型预加载失败:', {
|
console.warn('[ThreeMap] 相邻楼层模型预加载失败:', {
|
||||||
urls: candidates,
|
urls: availableCandidates,
|
||||||
label,
|
label,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error)
|
||||||
})
|
})
|
||||||
@@ -2497,10 +2709,25 @@ const scheduleAdjacentFloorPreload = (floorId: string) => {
|
|||||||
if (preloadSeq !== adjacentPreloadSeq || isDisposed) return
|
if (preloadSeq !== adjacentPreloadSeq || isDisposed) return
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
const seenModelKeys = new Set<string>()
|
||||||
for (const floor of adjacentFloors) {
|
for (const floor of adjacentFloors) {
|
||||||
if (preloadSeq !== adjacentPreloadSeq || isDisposed) return
|
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(
|
await preloadModelUrls(
|
||||||
getFloorModelUrls(floor),
|
uniqueUrls,
|
||||||
`预加载 ${formatFloorLabel(floor.floorId)} 模型`,
|
`预加载 ${formatFloorLabel(floor.floorId)} 模型`,
|
||||||
preloadSeq
|
preloadSeq
|
||||||
)
|
)
|
||||||
@@ -2980,12 +3207,13 @@ const fitCameraToObject = (object: THREE.Object3D, preset: CameraPreset = 'obliq
|
|||||||
const overviewFitOptions: CameraFitOptions = activeView.value === 'overview'
|
const overviewFitOptions: CameraFitOptions = activeView.value === 'overview'
|
||||||
? {
|
? {
|
||||||
distanceFactor: 0.9,
|
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)
|
up: new THREE.Vector3(0, 1, 0)
|
||||||
}
|
}
|
||||||
: activeView.value === 'floor'
|
: activeView.value === 'floor'
|
||||||
? {
|
? {
|
||||||
distanceFactor: FLOOR_CAMERA_DISTANCE_FACTOR,
|
distanceFactor: FLOOR_CAMERA_DISTANCE_FACTOR,
|
||||||
|
screenOffsetRatio: SGS_VISUAL_RENDER_CONFIG.framing.floorScreenOffsetRatio,
|
||||||
up: new THREE.Vector3(0, 1, 0)
|
up: new THREE.Vector3(0, 1, 0)
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
@@ -3069,9 +3297,20 @@ const loadCurrentFloorPoiMarkers = async (loadToken: number) => {
|
|||||||
const loadCurrentFloorPoiMarkersInBackground = (loadToken: number) => {
|
const loadCurrentFloorPoiMarkersInBackground = (loadToken: number) => {
|
||||||
if (!shouldRenderPoiMarkers.value) return
|
if (!shouldRenderPoiMarkers.value) return
|
||||||
|
|
||||||
|
const startedAt = getNow()
|
||||||
|
logThreeMapDiagnostic('poi-background-start', {
|
||||||
|
floorId: currentFloor.value,
|
||||||
|
view: activeView.value
|
||||||
|
})
|
||||||
|
|
||||||
void loadCurrentFloorPoiMarkers(loadToken)
|
void loadCurrentFloorPoiMarkers(loadToken)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!isCurrentModelLoad(loadToken)) return
|
if (!isCurrentModelLoad(loadToken)) return
|
||||||
|
logThreeMapDiagnostic('poi-background-ready', {
|
||||||
|
floorId: currentFloor.value,
|
||||||
|
view: activeView.value,
|
||||||
|
elapsedMs: Math.round(getNow() - startedAt)
|
||||||
|
})
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
renderRoutePreview()
|
renderRoutePreview()
|
||||||
})
|
})
|
||||||
@@ -3103,6 +3342,10 @@ const loadOverview = async () => {
|
|||||||
resetAutoSwitchDistanceTracking()
|
resetAutoSwitchDistanceTracking()
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
renderRoutePreview()
|
renderRoutePreview()
|
||||||
|
markFirstModelVisible('overview', {
|
||||||
|
source: 'cached-overview',
|
||||||
|
modelUrl: cachedSharedModelUrl
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3130,6 +3373,10 @@ const loadOverview = async () => {
|
|||||||
resetAutoSwitchDistanceTracking()
|
resetAutoSwitchDistanceTracking()
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
renderRoutePreview()
|
renderRoutePreview()
|
||||||
|
markFirstModelVisible('overview', {
|
||||||
|
source: 'network',
|
||||||
|
modelUrl: packageData.overviewModelUrl
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const prepareFloorScene = async (
|
const prepareFloorScene = async (
|
||||||
@@ -3190,17 +3437,9 @@ const prepareFloorScene = async (
|
|||||||
throw new Error(`楼层模型加载失败:${requestedFloorId}`)
|
throw new Error(`楼层模型加载失败:${requestedFloorId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
let poiEntry: PoiMarkerCacheEntry | undefined | null = null
|
const poiEntry = shouldRenderPoiMarkers.value
|
||||||
try {
|
? poiMarkerGroupCache.get(getPoiMarkerCacheKey(floor.floorId)) || null
|
||||||
poiEntry = shouldRenderPoiMarkers.value
|
|
||||||
? await prepareFloorPOIs(floor, loadToken, model)
|
|
||||||
: null
|
: null
|
||||||
} catch (error) {
|
|
||||||
if (ownsModel) {
|
|
||||||
disposeObject(model, protectedResources)
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
const prepared: PreparedFloorScene = {
|
const prepared: PreparedFloorScene = {
|
||||||
floor,
|
floor,
|
||||||
@@ -3260,6 +3499,12 @@ const commitPreparedFloorScene = (
|
|||||||
markFloorAutoSwitchEntry()
|
markFloorAutoSwitchEntry()
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
renderRoutePreview()
|
renderRoutePreview()
|
||||||
|
markFirstModelVisible('floor', {
|
||||||
|
floorId: expectedFloorId,
|
||||||
|
modelUrl: prepared.floor.modelUrl,
|
||||||
|
poiFromCache: Boolean(prepared.poiEntry)
|
||||||
|
})
|
||||||
|
loadCurrentFloorPoiMarkersInBackground(loadToken)
|
||||||
scheduleAdjacentFloorPreload(expectedFloorId)
|
scheduleAdjacentFloorPreload(expectedFloorId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3370,6 +3615,11 @@ const loadMultiFloor = async () => {
|
|||||||
resetAutoSwitchDistanceTracking()
|
resetAutoSwitchDistanceTracking()
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
renderRoutePreview()
|
renderRoutePreview()
|
||||||
|
markFirstModelVisible('multi', {
|
||||||
|
source: 'shared-model',
|
||||||
|
modelUrl: sharedModelUrl,
|
||||||
|
floorCount: orderedFloors.length
|
||||||
|
})
|
||||||
return
|
return
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
disposeObject(group, protectedResources)
|
disposeObject(group, protectedResources)
|
||||||
@@ -3441,6 +3691,10 @@ const loadMultiFloor = async () => {
|
|||||||
resetAutoSwitchDistanceTracking()
|
resetAutoSwitchDistanceTracking()
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
renderRoutePreview()
|
renderRoutePreview()
|
||||||
|
markFirstModelVisible('multi', {
|
||||||
|
source: 'per-floor-models',
|
||||||
|
floorCount: orderedFloors.length
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const createPoiMaterial = (poi: RenderPoi) => {
|
const createPoiMaterial = (poi: RenderPoi) => {
|
||||||
@@ -3586,6 +3840,7 @@ const createPoiLabelSprite = (poi: RenderPoi, markerSize: number) => {
|
|||||||
sprite.userData.labelBaseScaleX = markerSize * 5.6
|
sprite.userData.labelBaseScaleX = markerSize * 5.6
|
||||||
sprite.userData.labelBaseScaleY = markerSize * 1.68
|
sprite.userData.labelBaseScaleY = markerSize * 1.68
|
||||||
sprite.renderOrder = 30
|
sprite.renderOrder = 30
|
||||||
|
sprite.frustumCulled = false
|
||||||
sprite.scale.set(sprite.userData.labelBaseScaleX, sprite.userData.labelBaseScaleY, 1)
|
sprite.scale.set(sprite.userData.labelBaseScaleX, sprite.userData.labelBaseScaleY, 1)
|
||||||
|
|
||||||
return sprite
|
return sprite
|
||||||
@@ -3661,6 +3916,7 @@ const createAmbientPoiLabelSprite = (poi: RenderPoi, markerSize: number) => {
|
|||||||
sprite.userData.labelBaseScaleX = markerSize * (isHallPoi(poi) ? 3.9 : 3.05)
|
sprite.userData.labelBaseScaleX = markerSize * (isHallPoi(poi) ? 3.9 : 3.05)
|
||||||
sprite.userData.labelBaseScaleY = markerSize * (isHallPoi(poi) ? 1.22 : 0.95)
|
sprite.userData.labelBaseScaleY = markerSize * (isHallPoi(poi) ? 1.22 : 0.95)
|
||||||
sprite.renderOrder = isHallPoi(poi) ? 18 : 15
|
sprite.renderOrder = isHallPoi(poi) ? 18 : 15
|
||||||
|
sprite.frustumCulled = false
|
||||||
sprite.scale.set(sprite.userData.labelBaseScaleX, sprite.userData.labelBaseScaleY, 1)
|
sprite.scale.set(sprite.userData.labelBaseScaleX, sprite.userData.labelBaseScaleY, 1)
|
||||||
sprite.visible = false
|
sprite.visible = false
|
||||||
|
|
||||||
@@ -3697,6 +3953,7 @@ const createPoiPulseSprite = (poi: RenderPoi, markerSize: number) => {
|
|||||||
sprite.userData.isPoiPulse = true
|
sprite.userData.isPoiPulse = true
|
||||||
sprite.userData.baseScale = markerSize * 1.9
|
sprite.userData.baseScale = markerSize * 1.9
|
||||||
sprite.renderOrder = 9
|
sprite.renderOrder = 9
|
||||||
|
sprite.frustumCulled = false
|
||||||
sprite.scale.set(markerSize * 1.9, markerSize * 1.9, markerSize * 1.9)
|
sprite.scale.set(markerSize * 1.9, markerSize * 1.9, markerSize * 1.9)
|
||||||
|
|
||||||
return sprite
|
return sprite
|
||||||
@@ -3734,6 +3991,7 @@ const createPoiBaseSprite = (poi: RenderPoi, markerSize: number) => {
|
|||||||
sprite.userData.isPoiBase = true
|
sprite.userData.isPoiBase = true
|
||||||
sprite.userData.baseScale = markerSize * 2.15
|
sprite.userData.baseScale = markerSize * 2.15
|
||||||
sprite.renderOrder = 8
|
sprite.renderOrder = 8
|
||||||
|
sprite.frustumCulled = false
|
||||||
sprite.scale.set(markerSize * 2.15, markerSize * 2.15, markerSize * 2.15)
|
sprite.scale.set(markerSize * 2.15, markerSize * 2.15, markerSize * 2.15)
|
||||||
|
|
||||||
return sprite
|
return sprite
|
||||||
@@ -3771,7 +4029,8 @@ const showFocusPoiLabel = (poi: RenderPoi) => {
|
|||||||
|
|
||||||
const markerSize = getPoiMarkerSize()
|
const markerSize = getPoiMarkerSize()
|
||||||
activeFocusLabelSprite = createPoiLabelSprite(poi, markerSize)
|
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()
|
updateFocusLabelScale()
|
||||||
poiGroup.add(activeFocusLabelSprite)
|
poiGroup.add(activeFocusLabelSprite)
|
||||||
}
|
}
|
||||||
@@ -3784,7 +4043,8 @@ const showFocusPoiBase = (poi: RenderPoi) => {
|
|||||||
|
|
||||||
const markerSize = getPoiMarkerSize()
|
const markerSize = getPoiMarkerSize()
|
||||||
activeFocusBaseSprite = createPoiBaseSprite(poi, markerSize)
|
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)
|
poiGroup.add(activeFocusBaseSprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3796,7 +4056,8 @@ const showFocusPoiPulse = (poi: RenderPoi) => {
|
|||||||
|
|
||||||
const markerSize = getPoiMarkerSize()
|
const markerSize = getPoiMarkerSize()
|
||||||
activeFocusPulseSprite = createPoiPulseSprite(poi, markerSize)
|
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)
|
poiGroup.add(activeFocusPulseSprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3809,6 +4070,7 @@ const showFocusPoiAffordances = (poi: RenderPoi) => {
|
|||||||
|
|
||||||
const getPoiColor = (category: string) => {
|
const getPoiColor = (category: string) => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
|
poi: '#56606b',
|
||||||
touring_poi: '#2f6fed',
|
touring_poi: '#2f6fed',
|
||||||
exhibition_hall: '#2f6fed',
|
exhibition_hall: '#2f6fed',
|
||||||
exhibition_hall_entrance: '#4659d8',
|
exhibition_hall_entrance: '#4659d8',
|
||||||
@@ -3900,6 +4162,7 @@ const createPoiMarkerSprites = (poi: RenderPoi, markerSize: number) => {
|
|||||||
sprite.userData.poi = poi
|
sprite.userData.poi = poi
|
||||||
sprite.userData.isCorePoi = isCorePoi
|
sprite.userData.isCorePoi = isCorePoi
|
||||||
sprite.userData.hitTarget = hitTarget
|
sprite.userData.hitTarget = hitTarget
|
||||||
|
sprite.frustumCulled = false
|
||||||
if (labelSprite) {
|
if (labelSprite) {
|
||||||
sprite.userData.labelSprite = labelSprite
|
sprite.userData.labelSprite = labelSprite
|
||||||
}
|
}
|
||||||
@@ -3910,6 +4173,7 @@ const createPoiMarkerSprites = (poi: RenderPoi, markerSize: number) => {
|
|||||||
hitTarget.userData.visibleMarker = sprite
|
hitTarget.userData.visibleMarker = sprite
|
||||||
hitTarget.userData.isPoiHitTarget = true
|
hitTarget.userData.isPoiHitTarget = true
|
||||||
hitTarget.renderOrder = 11
|
hitTarget.renderOrder = 11
|
||||||
|
hitTarget.frustumCulled = false
|
||||||
hitTarget.scale.set(hitTargetScale, hitTargetScale, hitTargetScale)
|
hitTarget.scale.set(hitTargetScale, hitTargetScale, hitTargetScale)
|
||||||
|
|
||||||
setPoiSpriteFocusStyle(sprite, poi.id === activeFocusPoiId.value)
|
setPoiSpriteFocusStyle(sprite, poi.id === activeFocusPoiId.value)
|
||||||
@@ -3941,7 +4205,7 @@ const addFocusMarkerFromRequest = (request: TargetPoiFocusRequest) => {
|
|||||||
|
|
||||||
const markerSize = getPoiMarkerSize()
|
const markerSize = getPoiMarkerSize()
|
||||||
const { sprite, hitTarget } = createPoiMarkerSprites(poi, markerSize)
|
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)
|
hitTarget.position.copy(sprite.position)
|
||||||
poiGroup.add(sprite)
|
poiGroup.add(sprite)
|
||||||
poiGroup.add(hitTarget)
|
poiGroup.add(hitTarget)
|
||||||
@@ -3966,10 +4230,10 @@ const createPoiMarkerGroup = (
|
|||||||
if (!displayPosition) return
|
if (!displayPosition) return
|
||||||
|
|
||||||
const { sprite, hitTarget, labelSprite } = createPoiMarkerSprites(poi, markerSize)
|
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)
|
hitTarget.position.copy(sprite.position)
|
||||||
if (labelSprite) {
|
if (labelSprite) {
|
||||||
labelSprite.position.set(displayPosition.x, displayPosition.y + markerSize * 1.82, displayPosition.z)
|
labelSprite.position.copy(getPoiAmbientLabelPosition(displayPosition, markerSize))
|
||||||
labelSprite.visible = false
|
labelSprite.visible = false
|
||||||
}
|
}
|
||||||
sprite.visible = shouldShowPoiAtDistance(poi, getPoiVisibilityTier())
|
sprite.visible = shouldShowPoiAtDistance(poi, getPoiVisibilityTier())
|
||||||
@@ -3994,6 +4258,24 @@ const attachPoiMarkerGroup = (entry: PoiMarkerCacheEntry) => {
|
|||||||
poiGroup.userData.floorId = entry.floorId
|
poiGroup.userData.floorId = entry.floorId
|
||||||
poiGroup.userData.currentFloor = entry.floorId
|
poiGroup.userData.currentFloor = entry.floorId
|
||||||
updatePoiMarkerFocus()
|
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 = () => {
|
const restoreCommittedFloorPoiLayer = () => {
|
||||||
@@ -4044,6 +4326,21 @@ const prepareFloorPOIs = async (
|
|||||||
|
|
||||||
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 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
|
const displayOffsetY = markerModel
|
||||||
? resolvePoiDisplayOffsetY(floor.floorId, markerModel, validPois)
|
? resolvePoiDisplayOffsetY(floor.floorId, markerModel, validPois)
|
||||||
: getCachedPoiDisplayOffsetY(floor.floorId)
|
: getCachedPoiDisplayOffsetY(floor.floorId)
|
||||||
@@ -4055,7 +4352,10 @@ const prepareFloorPOIs = async (
|
|||||||
pois: validPois,
|
pois: validPois,
|
||||||
group: markerGroup,
|
group: markerGroup,
|
||||||
markerSize,
|
markerSize,
|
||||||
displayOffsetY
|
displayOffsetY,
|
||||||
|
rawPoiCount: floorPois.length,
|
||||||
|
rawPositionedPoiCount: floorPois.filter((poi) => Boolean(poi.positionGltf)).length,
|
||||||
|
filteredCategoryCounts: countPoisByCategory(filteredPois)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
||||||
@@ -4140,13 +4440,31 @@ const handleSceneTap = async (event: PointerEvent) => {
|
|||||||
raycaster.setFromCamera(pointer, camera)
|
raycaster.setFromCamera(pointer, camera)
|
||||||
|
|
||||||
if ((activeView.value === 'floor' || activeView.value === 'overview') && poiGroup) {
|
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) => (
|
const hit = poiHits.find((item) => (
|
||||||
item.object.visible
|
item.object.visible
|
||||||
&& item.object instanceof THREE.Sprite
|
&& item.object instanceof THREE.Sprite
|
||||||
&& getPoiMarkerFromHit(item.object)
|
&& 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) {
|
if (hitMarker?.userData.poi) {
|
||||||
const selectedPoi = hitMarker.userData.poi as RenderPoi
|
const selectedPoi = hitMarker.userData.poi as RenderPoi
|
||||||
@@ -4352,6 +4670,17 @@ const loadModelPackage = async () => {
|
|||||||
|
|
||||||
setProgress(14, '正在读取楼层索引...')
|
setProgress(14, '正在读取楼层索引...')
|
||||||
floorIndex.value = [...packageData.floors].sort(compareFloorsTopToBottom)
|
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)
|
const requestedInitialFloorId = resolveFloorIdFromRequest(props.initialFloorId)
|
||||||
if (requestedInitialFloorId) {
|
if (requestedInitialFloorId) {
|
||||||
@@ -4380,11 +4709,17 @@ const init3DScene = async () => {
|
|||||||
try {
|
try {
|
||||||
disposeScene()
|
disposeScene()
|
||||||
isDisposed = false
|
isDisposed = false
|
||||||
|
firstModelLoadStartedAt = getNow()
|
||||||
|
firstModelVisibleReported = false
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
loadError.value = false
|
loadError.value = false
|
||||||
selectedPOI.value = null
|
selectedPOI.value = null
|
||||||
hasLoadedFloorViewOnce = false
|
hasLoadedFloorViewOnce = false
|
||||||
setProgress(0, '正在初始化三维场景...')
|
setProgress(0, '正在初始化三维场景...')
|
||||||
|
logThreeMapDiagnostic('init-start', {
|
||||||
|
initialView: props.initialView,
|
||||||
|
initialFloorId: props.initialFloorId
|
||||||
|
})
|
||||||
|
|
||||||
await initThree()
|
await initThree()
|
||||||
await loadModelPackage()
|
await loadModelPackage()
|
||||||
@@ -4780,7 +5115,8 @@ onUnmounted(() => {
|
|||||||
border: 1px solid rgba(31, 35, 41, 0.1);
|
border: 1px solid rgba(31, 35, 41, 0.1);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 10px 28px rgba(31, 35, 41, 0.16);
|
box-shadow: 0 10px 28px rgba(31, 35, 41, 0.16);
|
||||||
z-index: 14;
|
z-index: 60;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poi-content {
|
.poi-content {
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ import type {
|
|||||||
} from '@/data/providers/sgsSdkApiProvider'
|
} from '@/data/providers/sgsSdkApiProvider'
|
||||||
|
|
||||||
const defaultCategory: MuseumCategory = {
|
const defaultCategory: MuseumCategory = {
|
||||||
id: 'operation_experience',
|
id: 'poi',
|
||||||
label: '导览点位',
|
label: '点位',
|
||||||
iconType: 'poi'
|
iconType: 'poi'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +55,21 @@ const hallEntranceCategory: MuseumCategory = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const categoryBySgsType: Record<string, MuseumCategory & { accessible?: boolean }> = {
|
const categoryBySgsType: Record<string, MuseumCategory & { accessible?: boolean }> = {
|
||||||
|
poi: {
|
||||||
|
id: 'poi',
|
||||||
|
label: '点位',
|
||||||
|
iconType: 'poi'
|
||||||
|
},
|
||||||
|
device_terminal: {
|
||||||
|
id: 'poi',
|
||||||
|
label: '点位',
|
||||||
|
iconType: 'poi'
|
||||||
|
},
|
||||||
|
operation_experience: {
|
||||||
|
id: 'operation_experience',
|
||||||
|
label: '导览点位',
|
||||||
|
iconType: 'poi'
|
||||||
|
},
|
||||||
toilet: {
|
toilet: {
|
||||||
id: 'basic_service_facility',
|
id: 'basic_service_facility',
|
||||||
label: '卫生间',
|
label: '卫生间',
|
||||||
@@ -412,6 +427,10 @@ const categoryFor = (poi: SgsPoiPayload): MuseumCategory & { accessible?: boolea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (String(poi.poiGroup || '').toUpperCase() === 'OTHER') {
|
||||||
|
return categoryBySgsType[normalizedType || ''] || defaultCategory
|
||||||
|
}
|
||||||
|
|
||||||
if (normalizedType && categoryBySgsType[normalizedType]) {
|
if (normalizedType && categoryBySgsType[normalizedType]) {
|
||||||
return categoryBySgsType[normalizedType]
|
return categoryBySgsType[normalizedType]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,24 +168,75 @@ const parseJsonPayload = <T>(payload: unknown): T => {
|
|||||||
return payload as T
|
return payload as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNow = () => (
|
||||||
|
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
||||||
|
? performance.now()
|
||||||
|
: Date.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
const isStaticNavDiagnosticsEnabled = () => {
|
||||||
|
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 logStaticNavDiagnostic = (
|
||||||
|
event: 'request-json' | 'request-json-complete' | 'request-json-failed',
|
||||||
|
payload: Record<string, unknown>
|
||||||
|
) => {
|
||||||
|
if (!isStaticNavDiagnosticsEnabled()) return
|
||||||
|
console.debug(`[StaticNavAssetsProvider] ${event}`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
const requestJson = <T>(url: string): Promise<T> => new Promise((resolve, reject) => {
|
const requestJson = <T>(url: string): Promise<T> => new Promise((resolve, reject) => {
|
||||||
|
const startedAt = getNow()
|
||||||
|
logStaticNavDiagnostic('request-json', { url })
|
||||||
|
|
||||||
uni.request({
|
uni.request({
|
||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
success: (response) => {
|
success: (response) => {
|
||||||
const statusCode = Number(response.statusCode || 0)
|
const statusCode = Number(response.statusCode || 0)
|
||||||
if (statusCode < 200 || statusCode >= 300) {
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
logStaticNavDiagnostic('request-json-failed', {
|
||||||
|
url,
|
||||||
|
statusCode,
|
||||||
|
elapsedMs: Math.round(getNow() - startedAt)
|
||||||
|
})
|
||||||
reject(new Error(`导览资源读取失败: ${statusCode} ${url}`))
|
reject(new Error(`导览资源读取失败: ${statusCode} ${url}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resolve(parseJsonPayload<T>(response.data))
|
const data = parseJsonPayload<T>(response.data)
|
||||||
|
logStaticNavDiagnostic('request-json-complete', {
|
||||||
|
url,
|
||||||
|
statusCode,
|
||||||
|
elapsedMs: Math.round(getNow() - startedAt)
|
||||||
|
})
|
||||||
|
resolve(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logStaticNavDiagnostic('request-json-failed', {
|
||||||
|
url,
|
||||||
|
statusCode,
|
||||||
|
elapsedMs: Math.round(getNow() - startedAt),
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
reject(error)
|
reject(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: reject
|
fail: (error) => {
|
||||||
|
logStaticNavDiagnostic('request-json-failed', {
|
||||||
|
url,
|
||||||
|
elapsedMs: Math.round(getNow() - startedAt),
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ import {
|
|||||||
|
|
||||||
const SDK_FLOOR_BUNDLE_CONCURRENCY = 2
|
const SDK_FLOOR_BUNDLE_CONCURRENCY = 2
|
||||||
|
|
||||||
|
const getNow = () => (
|
||||||
|
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
||||||
|
? performance.now()
|
||||||
|
: Date.now()
|
||||||
|
)
|
||||||
|
|
||||||
const toRenderPoi = (poi: StaticNavPoiPayload): GuideRenderPoi => ({
|
const toRenderPoi = (poi: StaticNavPoiPayload): GuideRenderPoi => ({
|
||||||
id: poi.id,
|
id: poi.id,
|
||||||
name: poi.name,
|
name: poi.name,
|
||||||
@@ -77,6 +83,26 @@ const dedupeRenderPoisById = (pois: GuideRenderPoi[]) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const countByValue = <T>(
|
||||||
|
items: T[],
|
||||||
|
selector: (item: T) => string | number | null | undefined
|
||||||
|
) => items.reduce<Record<string, number>>((counts, item) => {
|
||||||
|
const key = String(selector(item) || '<missing>')
|
||||||
|
counts[key] = (counts[key] || 0) + 1
|
||||||
|
return counts
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const countDroppedRenderPoiCategories = (
|
||||||
|
sourcePois: GuideRenderPoi[],
|
||||||
|
keptPois: GuideRenderPoi[]
|
||||||
|
) => {
|
||||||
|
const keptIds = new Set(keptPois.map((poi) => poi.id))
|
||||||
|
return countByValue(
|
||||||
|
sourcePois.filter((poi) => !keptIds.has(poi.id)),
|
||||||
|
(poi) => poi.primaryCategory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, '')
|
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, '')
|
||||||
|
|
||||||
const resolveSgsAssetUrl = (url?: string | null) => {
|
const resolveSgsAssetUrl = (url?: string | null) => {
|
||||||
@@ -135,6 +161,37 @@ const warnSgsGuideModelDiagnostics = (
|
|||||||
console.warn(`[SGS guide model] ${message}`, payload)
|
console.warn(`[SGS guide model] ${message}`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stringifyDiagnosticPayload = (payload: Record<string, unknown>) => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(payload)
|
||||||
|
} catch {
|
||||||
|
return '[unserializable]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logSgsGuideModelDiagnostics = (
|
||||||
|
message: string,
|
||||||
|
payload: Record<string, unknown>
|
||||||
|
) => {
|
||||||
|
if (!import.meta.env.DEV) return
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
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: 'SGS guide model',
|
||||||
|
event: message,
|
||||||
|
payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.debug(`[SGS guide model] ${message} ${stringifyDiagnosticPayload(payload)}`)
|
||||||
|
}
|
||||||
|
|
||||||
const summarizeYValues = (pois: GuideRenderPoi[]) => {
|
const summarizeYValues = (pois: GuideRenderPoi[]) => {
|
||||||
const values = pois
|
const values = pois
|
||||||
.map((poi) => poi.positionGltf?.[1])
|
.map((poi) => poi.positionGltf?.[1])
|
||||||
@@ -228,9 +285,22 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
|||||||
constructor(private readonly provider: SgsSdkApiProvider = defaultSgsSdkApiProvider) {}
|
constructor(private readonly provider: SgsSdkApiProvider = defaultSgsSdkApiProvider) {}
|
||||||
|
|
||||||
async loadPackage(): Promise<GuideModelRenderPackage> {
|
async loadPackage(): Promise<GuideModelRenderPackage> {
|
||||||
|
const startedAt = getNow()
|
||||||
const manifest = await this.provider.getManifest()
|
const manifest = await this.provider.getManifest()
|
||||||
const allFloors = sortSgsFloors(manifest.floors)
|
const allFloors = sortSgsFloors(manifest.floors)
|
||||||
const floors = allFloors.filter(isIndoorNavigableFloor)
|
const floors = allFloors.filter(isIndoorNavigableFloor)
|
||||||
|
const overviewFloor = allFloors.find((floor) => !isIndoorNavigableFloor(floor))
|
||||||
|
const overviewBundleRequest = overviewFloor
|
||||||
|
? this.provider.getFloorBundle(String(overviewFloor.floorId)).catch((error) => {
|
||||||
|
warnSgsGuideModelDiagnostics('overview bundle request failed', {
|
||||||
|
floorId: String(overviewFloor.floorId),
|
||||||
|
floorCode: overviewFloor.floorCode,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
: Promise.resolve(null)
|
||||||
|
|
||||||
this.floorsCache = floors
|
this.floorsCache = floors
|
||||||
|
|
||||||
const bundleResults = await mapWithConcurrency(
|
const bundleResults = await mapWithConcurrency(
|
||||||
@@ -291,10 +361,7 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
|||||||
})
|
})
|
||||||
.filter((asset): asset is GuideModelFloorAsset => Boolean(asset?.modelUrl))
|
.filter((asset): asset is GuideModelFloorAsset => Boolean(asset?.modelUrl))
|
||||||
|
|
||||||
const overviewFloor = allFloors.find((floor) => !isIndoorNavigableFloor(floor))
|
const overviewBundle = await overviewBundleRequest
|
||||||
const overviewBundle = overviewFloor
|
|
||||||
? await this.provider.getFloorBundle(String(overviewFloor.floorId)).catch(() => null)
|
|
||||||
: null
|
|
||||||
const overviewModelUrls = uniqueModelUrls([
|
const overviewModelUrls = uniqueModelUrls([
|
||||||
overviewBundle?.model?.modelUrl,
|
overviewBundle?.model?.modelUrl,
|
||||||
overviewBundle?.model?.fallbackModelUrl,
|
overviewBundle?.model?.fallbackModelUrl,
|
||||||
@@ -306,6 +373,15 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
|||||||
...asset,
|
...asset,
|
||||||
sharedModelAsset: isSharedSgsModelAsset(asset.modelUrl, overviewModelUrl, floorModelUrls)
|
sharedModelAsset: isSharedSgsModelAsset(asset.modelUrl, overviewModelUrl, floorModelUrls)
|
||||||
}))
|
}))
|
||||||
|
logSgsGuideModelDiagnostics('render package ready', {
|
||||||
|
elapsedMs: Math.round(getNow() - startedAt),
|
||||||
|
mapId: manifest.mapId,
|
||||||
|
mode: dataSourceConfig.mode,
|
||||||
|
overviewFloorId: overviewFloor ? String(overviewFloor.floorId) : '',
|
||||||
|
overviewModelUrl,
|
||||||
|
floorCount: floorAssets.length,
|
||||||
|
floorBundleCount: bundleResults.filter((result) => Boolean(result.bundle)).length
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
overviewModelUrl,
|
overviewModelUrl,
|
||||||
@@ -377,14 +453,28 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hallPois = museumHallPois.map(toSgsRenderPoi)
|
const hallPois = museumHallPois.map(toSgsRenderPoi)
|
||||||
const renderPois = dedupeRenderPoisById([
|
const adaptedPois = dedupeRenderPoisById([
|
||||||
...hallPois,
|
...hallPois,
|
||||||
...ordinaryPois
|
...ordinaryPois
|
||||||
])
|
])
|
||||||
.filter((poi) => poi.floorId === resolvedFloorId)
|
const floorMatchedPois = adaptedPois.filter((poi) => poi.floorId === resolvedFloorId)
|
||||||
|
const renderPois = floorMatchedPois
|
||||||
.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')
|
const renderHallPois = renderPois.filter((poi) => poi.kind === 'hall' || poi.primaryCategory === 'exhibition_hall')
|
||||||
|
logSgsGuideModelDiagnostics('floor POI category diagnostics', {
|
||||||
|
floorId: resolvedFloorId,
|
||||||
|
floorCode: matchedFloor.floorCode,
|
||||||
|
rawPoiCount: pois.length,
|
||||||
|
rawPoiTypeCounts: countByValue(pois, (poi) => poi.type),
|
||||||
|
rawPoiGroupCounts: countByValue(pois, (poi) => poi.poiGroup),
|
||||||
|
adaptedPoiCount: adaptedPois.length,
|
||||||
|
adaptedCategoryCounts: countByValue(adaptedPois, (poi) => poi.primaryCategory),
|
||||||
|
adaptedPoiCategoryCount: adaptedPois.filter((poi) => poi.primaryCategory === 'poi').length,
|
||||||
|
renderPoiCount: renderPois.length,
|
||||||
|
renderPoiCategoryCount: renderPois.filter((poi) => poi.primaryCategory === 'poi').length,
|
||||||
|
repositoryDroppedCategoryCounts: countDroppedRenderPoiCategories(adaptedPois, renderPois)
|
||||||
|
})
|
||||||
warnSgsGuideModelDiagnostics('floor render POI diagnostics', {
|
warnSgsGuideModelDiagnostics('floor render POI diagnostics', {
|
||||||
floorId: resolvedFloorId,
|
floorId: resolvedFloorId,
|
||||||
floorCode: matchedFloor.floorCode,
|
floorCode: matchedFloor.floorCode,
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ interface CachedModelEntry {
|
|||||||
lastUsedAt: number
|
lastUsedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GuideModelDiagnosticEvent =
|
||||||
|
| 'cache-hit'
|
||||||
|
| 'cache-miss'
|
||||||
|
| 'in-flight-hit'
|
||||||
|
| 'load-start'
|
||||||
|
| 'load-complete'
|
||||||
|
| 'load-failed'
|
||||||
|
| 'preload-skip'
|
||||||
|
|
||||||
// 当前 SDK 数据包含 8 个可导览楼层;缓存需覆盖多层展开、全楼模型和相邻预加载余量,避免加载中途淘汰刚加载过的单层模型。
|
// 当前 SDK 数据包含 8 个可导览楼层;缓存需覆盖多层展开、全楼模型和相邻预加载余量,避免加载中途淘汰刚加载过的单层模型。
|
||||||
const defaultMaxModelSourceEntries = 10
|
const defaultMaxModelSourceEntries = 10
|
||||||
|
|
||||||
@@ -53,6 +62,30 @@ const emptyResources = (): GuideModelResourceSet => ({
|
|||||||
textures: new Set()
|
textures: new Set()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getNow = () => (
|
||||||
|
typeof performance !== 'undefined' && typeof performance.now === 'function'
|
||||||
|
? performance.now()
|
||||||
|
: Date.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
const isModelDiagnosticsEnabled = () => {
|
||||||
|
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 logModelDiagnostic = (
|
||||||
|
event: GuideModelDiagnosticEvent,
|
||||||
|
payload: Record<string, unknown>
|
||||||
|
) => {
|
||||||
|
if (!isModelDiagnosticsEnabled()) return
|
||||||
|
console.debug(`[GuideModelLoadManager] ${event}`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
const collectMaterialTextures = (material: THREE.Material) => {
|
const collectMaterialTextures = (material: THREE.Material) => {
|
||||||
const textures = new Set<THREE.Texture>()
|
const textures = new Set<THREE.Texture>()
|
||||||
const materialRecord = material as unknown as Record<string, unknown>
|
const materialRecord = material as unknown as Record<string, unknown>
|
||||||
@@ -295,7 +328,16 @@ export class GuideModelLoadManager {
|
|||||||
const candidates = options.urls.map((url) => url.trim()).filter(Boolean)
|
const candidates = options.urls.map((url) => url.trim()).filter(Boolean)
|
||||||
for (const url of candidates) {
|
for (const url of candidates) {
|
||||||
const key = this.createCacheKey(url, options.semanticKey)
|
const key = this.createCacheKey(url, options.semanticKey)
|
||||||
if (!key || this.sourceCache.has(key) || this.inFlightCache.has(key)) return
|
if (!key || this.sourceCache.has(key) || this.inFlightCache.has(key)) {
|
||||||
|
logModelDiagnostic('preload-skip', {
|
||||||
|
label: options.label,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
reason: !key ? 'empty-key' : this.sourceCache.has(key) ? 'cache-hit' : 'in-flight-hit',
|
||||||
|
diagnostics: this.getDiagnostics()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
await this.loadSource(url, options)
|
await this.loadSource(url, options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -315,23 +357,60 @@ export class GuideModelLoadManager {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
cached.lastUsedAt = Date.now()
|
cached.lastUsedAt = Date.now()
|
||||||
this.diagnostics.hits += 1
|
this.diagnostics.hits += 1
|
||||||
|
logModelDiagnostic('cache-hit', {
|
||||||
|
label: options.label,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
diagnostics: this.getDiagnostics()
|
||||||
|
})
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
|
|
||||||
this.diagnostics.misses += 1
|
this.diagnostics.misses += 1
|
||||||
|
logModelDiagnostic('cache-miss', {
|
||||||
|
label: options.label,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
diagnostics: this.getDiagnostics()
|
||||||
|
})
|
||||||
|
|
||||||
let inFlight = this.inFlightCache.get(key)
|
let inFlight = this.inFlightCache.get(key)
|
||||||
|
const startedAt = getNow()
|
||||||
if (inFlight) {
|
if (inFlight) {
|
||||||
this.diagnostics.inFlightHits += 1
|
this.diagnostics.inFlightHits += 1
|
||||||
|
logModelDiagnostic('in-flight-hit', {
|
||||||
|
label: options.label,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
diagnostics: this.getDiagnostics()
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
|
logModelDiagnostic('load-start', {
|
||||||
|
label: options.label,
|
||||||
|
url,
|
||||||
|
key
|
||||||
|
})
|
||||||
inFlight = options.loadSource(url)
|
inFlight = options.loadSource(url)
|
||||||
this.inFlightCache.set(key, inFlight)
|
this.inFlightCache.set(key, inFlight)
|
||||||
}
|
}
|
||||||
|
|
||||||
const gltf = await inFlight.finally(() => {
|
let gltf: GLTF
|
||||||
|
try {
|
||||||
|
gltf = await inFlight
|
||||||
|
} catch (error) {
|
||||||
|
logModelDiagnostic('load-failed', {
|
||||||
|
label: options.label,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
elapsedMs: Math.round(getNow() - startedAt),
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
if (this.inFlightCache.get(key) === inFlight) {
|
if (this.inFlightCache.get(key) === inFlight) {
|
||||||
this.inFlightCache.delete(key)
|
this.inFlightCache.delete(key)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
const entry: CachedModelEntry = {
|
const entry: CachedModelEntry = {
|
||||||
key,
|
key,
|
||||||
@@ -341,6 +420,13 @@ export class GuideModelLoadManager {
|
|||||||
}
|
}
|
||||||
this.sourceCache.set(key, entry)
|
this.sourceCache.set(key, entry)
|
||||||
this.trim()
|
this.trim()
|
||||||
|
logModelDiagnostic('load-complete', {
|
||||||
|
label: options.label,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
elapsedMs: Math.round(getNow() - startedAt),
|
||||||
|
diagnostics: this.getDiagnostics()
|
||||||
|
})
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user