Compare commits
5 Commits
e813660aea
...
39f1fc1198
| Author | SHA1 | Date | |
|---|---|---|---|
| 39f1fc1198 | |||
| f03783201d | |||
| d8d7410736 | |||
| d47d4a2fdf | |||
| 5a61357f63 |
@@ -1,6 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="floor-switcher">
|
<view class="floor-switcher">
|
||||||
<view class="floor-list">
|
<view
|
||||||
|
class="floor-list"
|
||||||
|
:class="{
|
||||||
|
compact: floors.length >= 8,
|
||||||
|
dense: floors.length >= 10
|
||||||
|
}"
|
||||||
|
>
|
||||||
<view
|
<view
|
||||||
v-for="floor in floors"
|
v-for="floor in floors"
|
||||||
:key="floor.id"
|
:key="floor.id"
|
||||||
@@ -70,22 +76,33 @@ const handleFloorTap = (floorId: string) => {
|
|||||||
.floor-list {
|
.floor-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
background-color: rgba(26, 26, 46, 0.9);
|
background-color: rgba(26, 26, 46, 0.9);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
padding: 8px;
|
padding: 6px;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-list.compact {
|
||||||
|
gap: 3px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-list.dense {
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floor-item {
|
.floor-item {
|
||||||
width: 52px;
|
width: 46px;
|
||||||
height: 48px;
|
height: 34px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4px;
|
gap: 3px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
@@ -97,16 +114,27 @@ const handleFloorTap = (floorId: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floor-list.compact .floor-item {
|
||||||
|
width: 44px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-list.dense .floor-item {
|
||||||
|
width: 42px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
.floor-indicator {
|
.floor-indicator {
|
||||||
width: 24px;
|
width: 20px;
|
||||||
height: 3px;
|
height: 2px;
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floor-label {
|
.floor-label {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.6);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
@@ -117,7 +145,7 @@ const handleFloorTap = (floorId: string) => {
|
|||||||
background-color: rgba(224, 225, 0, 0.15);
|
background-color: rgba(224, 225, 0, 0.15);
|
||||||
|
|
||||||
.floor-indicator {
|
.floor-indicator {
|
||||||
width: 32px;
|
width: 28px;
|
||||||
background-color: #E0E100;
|
background-color: #E0E100;
|
||||||
box-shadow: 0 0 8px rgba(224, 225, 0, 0.5);
|
box-shadow: 0 0 8px rgba(224, 225, 0, 0.5);
|
||||||
}
|
}
|
||||||
@@ -143,8 +171,8 @@ const handleFloorTap = (floorId: string) => {
|
|||||||
|
|
||||||
// 3D 视角指示器
|
// 3D 视角指示器
|
||||||
.view-indicator {
|
.view-indicator {
|
||||||
width: 52px;
|
width: 46px;
|
||||||
height: 28px;
|
height: 24px;
|
||||||
background: linear-gradient(135deg, #E0E100 0%, #c4c400 100%);
|
background: linear-gradient(135deg, #E0E100 0%, #c4c400 100%);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -153,7 +181,7 @@ const handleFloorTap = (floorId: string) => {
|
|||||||
box-shadow: 0 2px 8px rgba(224, 225, 0, 0.4);
|
box-shadow: 0 2px 8px rgba(224, 225, 0, 0.4);
|
||||||
|
|
||||||
.view-icon {
|
.view-icon {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -141,17 +141,11 @@
|
|||||||
<text class="floor-header-icon">▱</text>
|
<text class="floor-header-icon">▱</text>
|
||||||
<text class="floor-header-label">{{ layerModeActionLabel }}</text>
|
<text class="floor-header-label">{{ layerModeActionLabel }}</text>
|
||||||
</view>
|
</view>
|
||||||
<scroll-view
|
<view
|
||||||
class="floor-list"
|
class="floor-list"
|
||||||
:style="floorListStyle"
|
|
||||||
scroll-y
|
|
||||||
:show-scrollbar="false"
|
|
||||||
:scroll-into-view="activeFloorScrollIntoView"
|
|
||||||
:scroll-with-animation="true"
|
|
||||||
>
|
>
|
||||||
<view
|
<view
|
||||||
v-for="floor in floorItems"
|
v-for="floor in floorItems"
|
||||||
:id="floor.scrollId"
|
|
||||||
:key="floor.id"
|
:key="floor.id"
|
||||||
class="floor-item"
|
class="floor-item"
|
||||||
:class="{
|
:class="{
|
||||||
@@ -164,7 +158,7 @@
|
|||||||
>
|
>
|
||||||
<text class="floor-label">{{ floor.label }}</text>
|
<text class="floor-label">{{ floor.label }}</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="showZoomControls" class="zoom-controls" :style="zoomControlsStyle">
|
<view v-if="showZoomControls" class="zoom-controls" :style="zoomControlsStyle">
|
||||||
@@ -437,11 +431,7 @@ const indoorRendererRef = ref<{
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const indoorFloors = computed(() => props.floors.filter((floor) => isIndoorNavigableFloor(floor)))
|
const indoorFloors = computed(() => props.floors.filter((floor) => isIndoorNavigableFloor(floor)))
|
||||||
const createFloorScrollId = (floorId: string) => `guide-floor-${floorId.replace(/[^a-zA-Z0-9_-]/g, '-')}`
|
const floorItems = computed(() => indoorFloors.value)
|
||||||
const floorItems = computed(() => indoorFloors.value.map((floor) => ({
|
|
||||||
...floor,
|
|
||||||
scrollId: createFloorScrollId(floor.id)
|
|
||||||
})))
|
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
const effectiveIndoorModelSource = computed(() => props.indoorModelSource)
|
const effectiveIndoorModelSource = computed(() => props.indoorModelSource)
|
||||||
// #endif
|
// #endif
|
||||||
@@ -461,7 +451,6 @@ const activeFloorId = computed(() => {
|
|||||||
))
|
))
|
||||||
return normalizedFloor?.id || normalizedFloorId
|
return normalizedFloor?.id || normalizedFloorId
|
||||||
})
|
})
|
||||||
const activeFloorScrollIntoView = ref('')
|
|
||||||
const requestedFloorId = ref('')
|
const requestedFloorId = ref('')
|
||||||
const requestedFloorLabel = ref('')
|
const requestedFloorLabel = ref('')
|
||||||
const loadingFloorId = ref('')
|
const loadingFloorId = ref('')
|
||||||
@@ -469,38 +458,8 @@ const renderedFloorId = ref(activeFloorId.value)
|
|||||||
const activeFloorSwitchRequestSeq = ref(0)
|
const activeFloorSwitchRequestSeq = ref(0)
|
||||||
const renderedFloorSwitchRequestSeq = ref(0)
|
const renderedFloorSwitchRequestSeq = ref(0)
|
||||||
const failedFloorId = ref('')
|
const failedFloorId = ref('')
|
||||||
let suppressFloorAutoScrollForFloorId = ''
|
|
||||||
let floorSwitchRequestSeq = 0
|
let floorSwitchRequestSeq = 0
|
||||||
|
|
||||||
const syncActiveFloorScroll = () => {
|
|
||||||
if (props.mapType !== 'indoor' || props.indoorView === 'overview' || props.layerMode === 'multi') {
|
|
||||||
activeFloorScrollIntoView.value = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeFloorId.value === suppressFloorAutoScrollForFloorId) {
|
|
||||||
activeFloorScrollIntoView.value = ''
|
|
||||||
suppressFloorAutoScrollForFloorId = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
suppressFloorAutoScrollForFloorId = ''
|
|
||||||
|
|
||||||
const matchedFloor = floorItems.value.find((floor) => floor.id === activeFloorId.value)
|
|
||||||
activeFloorScrollIntoView.value = matchedFloor?.scrollId || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [
|
|
||||||
activeFloorId.value,
|
|
||||||
props.indoorView,
|
|
||||||
props.layerMode,
|
|
||||||
props.mapType,
|
|
||||||
floorItems.value.map((floor) => floor.id).join('|')
|
|
||||||
],
|
|
||||||
syncActiveFloorScroll,
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const searchFieldStyle = computed(() => ({
|
const searchFieldStyle = computed(() => ({
|
||||||
top: props.searchTop
|
top: props.searchTop
|
||||||
}))
|
}))
|
||||||
@@ -509,12 +468,6 @@ const floorSwitcherStyle = computed(() => ({
|
|||||||
...(props.floorBottom ? { bottom: props.floorBottom } : { top: props.floorTop })
|
...(props.floorBottom ? { bottom: props.floorBottom } : { top: props.floorTop })
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const floorListStyle = computed(() => ({
|
|
||||||
...(props.floorMaxHeight
|
|
||||||
? { maxHeight: props.showFloorHeader ? `calc(${props.floorMaxHeight} - 50px)` : props.floorMaxHeight }
|
|
||||||
: {})
|
|
||||||
}))
|
|
||||||
|
|
||||||
const layerModeToggleStyle = computed(() => ({
|
const layerModeToggleStyle = computed(() => ({
|
||||||
top: props.layerModeToggleTop
|
top: props.layerModeToggleTop
|
||||||
}))
|
}))
|
||||||
@@ -606,8 +559,6 @@ const handleFloorChange = (floor: { id: string; label: string }) => {
|
|||||||
const requestSeq = ++floorSwitchRequestSeq
|
const requestSeq = ++floorSwitchRequestSeq
|
||||||
activeFloorSwitchRequestSeq.value = requestSeq
|
activeFloorSwitchRequestSeq.value = requestSeq
|
||||||
failedFloorId.value = ''
|
failedFloorId.value = ''
|
||||||
suppressFloorAutoScrollForFloorId = floorId
|
|
||||||
activeFloorScrollIntoView.value = ''
|
|
||||||
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
|
indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000)
|
||||||
emit('floorRequest', {
|
emit('floorRequest', {
|
||||||
floorId,
|
floorId,
|
||||||
@@ -1067,12 +1018,11 @@ defineExpose({
|
|||||||
|
|
||||||
.floor-switcher {
|
.floor-switcher {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 46px;
|
width: 44px;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
scrollbar-width: none;
|
|
||||||
background: rgba(255, 255, 255, 0.84);
|
background: rgba(255, 255, 255, 0.84);
|
||||||
border: 1px solid #dde5df;
|
border: 1px solid #dde5df;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -1080,21 +1030,10 @@ defineExpose({
|
|||||||
z-index: 35;
|
z-index: 35;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floor-switcher::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floor-list {
|
.floor-list {
|
||||||
max-height: calc(100vh - 310px);
|
display: flex;
|
||||||
overflow-y: auto;
|
flex-direction: column;
|
||||||
overscroll-behavior: contain;
|
overflow: visible;
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
touch-action: pan-y;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floor-list::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.floor-switcher.side-right {
|
.floor-switcher.side-right {
|
||||||
@@ -1107,8 +1046,8 @@ defineExpose({
|
|||||||
|
|
||||||
.floor-header {
|
.floor-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 50px;
|
min-height: 38px;
|
||||||
padding: 7px 4px 6px;
|
padding: 5px 4px 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1134,8 +1073,8 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.floor-header-icon {
|
.floor-header-icon {
|
||||||
width: 18px;
|
width: 16px;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
color: #151713;
|
color: #151713;
|
||||||
@@ -1155,15 +1094,15 @@ defineExpose({
|
|||||||
.floor-header-icon::before {
|
.floor-header-icon::before {
|
||||||
left: 2px;
|
left: 2px;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
width: 12px;
|
width: 11px;
|
||||||
height: 10px;
|
height: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floor-header-icon::after {
|
.floor-header-icon::after {
|
||||||
left: 5px;
|
left: 5px;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
width: 12px;
|
width: 11px;
|
||||||
height: 10px;
|
height: 9px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1172,8 +1111,8 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.floor-header-label {
|
.floor-header-label {
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
line-height: 14px;
|
line-height: 12px;
|
||||||
color: #545861;
|
color: #545861;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1210,7 +1149,7 @@ defineExpose({
|
|||||||
|
|
||||||
.floor-item {
|
.floor-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 36px;
|
height: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -1247,7 +1186,7 @@ defineExpose({
|
|||||||
.floor-item.pending::before {
|
.floor-item.pending::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 7px;
|
inset: 5px;
|
||||||
border: 1px solid rgba(224, 225, 0, 0.9);
|
border: 1px solid rgba(224, 225, 0, 0.9);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
@@ -1262,8 +1201,8 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.floor-label {
|
.floor-label {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
line-height: 18px;
|
line-height: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #545861;
|
color: #545861;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -49,13 +49,6 @@
|
|||||||
<text class="audio-time">{{ exhibit.audio.durationLabel || '音频讲解' }}</text>
|
<text class="audio-time">{{ exhibit.audio.durationLabel || '音频讲解' }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="text-status-panel">
|
|
||||||
<text class="text-status-title">图文讲解</text>
|
|
||||||
<text class="text-status-desc">
|
|
||||||
{{ exhibit.audio.unavailableReason || '该讲解暂无已发布音频,当前提供图文讲解。' }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="content-section">
|
<view class="content-section">
|
||||||
<text class="section-title">讲解内容</text>
|
<text class="section-title">讲解内容</text>
|
||||||
<text class="section-text">{{ exhibit.body || exhibit.summary }}</text>
|
<text class="section-text">{{ exhibit.body || exhibit.summary }}</text>
|
||||||
@@ -478,8 +471,6 @@ const handleBack = () => {
|
|||||||
.detail-title,
|
.detail-title,
|
||||||
.detail-meta,
|
.detail-meta,
|
||||||
.audio-time,
|
.audio-time,
|
||||||
.text-status-title,
|
|
||||||
.text-status-desc,
|
|
||||||
.section-title,
|
.section-title,
|
||||||
.section-text,
|
.section-text,
|
||||||
.location-text {
|
.location-text {
|
||||||
@@ -551,29 +542,6 @@ const handleBack = () => {
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-status-panel {
|
|
||||||
margin-top: 24px;
|
|
||||||
padding: 14px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #e4e6df;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-status-title {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 21px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #151713;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-status-desc {
|
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 20px;
|
|
||||||
color: #5d6656;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section {
|
.content-section {
|
||||||
margin-top: 28px;
|
margin-top: 28px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1486,9 +1486,12 @@ const handleExplainBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.guide-quick-actions {
|
.guide-quick-actions {
|
||||||
|
--guide-floating-control-width: 48px;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 118px;
|
top: 118px;
|
||||||
right: 16px;
|
right: 18px;
|
||||||
|
width: var(--guide-floating-control-width);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -1497,9 +1500,10 @@ const handleExplainBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.guide-quick-action {
|
.guide-quick-action {
|
||||||
width: 76px;
|
width: 100%;
|
||||||
min-height: 46px;
|
height: 56px;
|
||||||
padding: 6px 7px;
|
min-height: 0;
|
||||||
|
padding: 6px 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1524,6 +1528,7 @@ const handleExplainBack = () => {
|
|||||||
.guide-quick-icon {
|
.guide-quick-icon {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
|
flex: 0 0 22px;
|
||||||
position: relative;
|
position: relative;
|
||||||
color: #151713;
|
color: #151713;
|
||||||
}
|
}
|
||||||
@@ -1575,11 +1580,13 @@ const handleExplainBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.guide-quick-action-text {
|
.guide-quick-action-text {
|
||||||
|
width: 100%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #151713;
|
color: #151713;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-quick-action-text.primary {
|
.guide-quick-action-text.primary {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export const EXHIBIT_PLACEHOLDER_IMAGE = '/static/icons/marker-exhibit.svg'
|
export const EXHIBIT_PLACEHOLDER_IMAGE = '/static/icons/marker-exhibit.svg'
|
||||||
|
export const EXPLAIN_DETAIL_PLACEHOLDER_IMAGE = '/static/explain/exhibit-detail-placeholder.jpg'
|
||||||
export const HALL_PLACEHOLDER_IMAGE = '/static/icons/marker-hall.svg'
|
export const HALL_PLACEHOLDER_IMAGE = '/static/icons/marker-hall.svg'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
SearchIndexItem
|
SearchIndexItem
|
||||||
} from '@/domain/museum'
|
} from '@/domain/museum'
|
||||||
import {
|
import {
|
||||||
EXHIBIT_PLACEHOLDER_IMAGE,
|
EXPLAIN_DETAIL_PLACEHOLDER_IMAGE,
|
||||||
HALL_PLACEHOLDER_IMAGE
|
HALL_PLACEHOLDER_IMAGE
|
||||||
} from '@/utils/placeholders'
|
} from '@/utils/placeholders'
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ export const toExplainDetailPageViewModel = (exhibit: MuseumExhibit): ExplainDet
|
|||||||
subtitle: buildSubtitle(exhibit),
|
subtitle: buildSubtitle(exhibit),
|
||||||
contentType: contentTypeFor(exhibit),
|
contentType: contentTypeFor(exhibit),
|
||||||
coverImages: [
|
coverImages: [
|
||||||
exhibit.image || EXHIBIT_PLACEHOLDER_IMAGE,
|
exhibit.image || EXPLAIN_DETAIL_PLACEHOLDER_IMAGE,
|
||||||
...(exhibit.galleryUrls || [])
|
...(exhibit.galleryUrls || [])
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
hallId: exhibit.hallId,
|
hallId: exhibit.hallId,
|
||||||
|
|||||||
BIN
static/explain/exhibit-detail-placeholder.jpg
Normal file
BIN
static/explain/exhibit-detail-placeholder.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
Reference in New Issue
Block a user