修复点位楼层跳转并优化模型加载缓存
This commit is contained in:
@@ -65,6 +65,10 @@ import type {
|
||||
import {
|
||||
compareFloorsTopToBottom
|
||||
} from '@/domain/guideFloor'
|
||||
import {
|
||||
guideModelLoadManager,
|
||||
type GuideModelResourceSet
|
||||
} from '@/services/model/GuideModelLoadManager'
|
||||
|
||||
type ViewMode = 'overview' | 'floor' | 'multi'
|
||||
type CameraPreset = 'top' | 'oblique'
|
||||
@@ -327,6 +331,7 @@ let overviewWheelAutoSwitchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let isProgrammaticCameraChange = false
|
||||
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let cameraTween: CameraTweenState | null = null
|
||||
let adjacentPreloadSeq = 0
|
||||
|
||||
const cameraTweenEnabled = true
|
||||
const cameraTweenDurationMs = 480
|
||||
@@ -351,6 +356,7 @@ const floorExitZoomOutIntentMinDeltaRatio = 0.015
|
||||
const floorExitAccumulatedZoomOutRatio = 0.26
|
||||
const floorExitCandidateHoldMs = 900
|
||||
const modelLoadRetryDelaysMs = [300, 900]
|
||||
const adjacentPreloadDelayMs = 900
|
||||
|
||||
const syncControlInteractionOptions = () => {
|
||||
if (!controls) return
|
||||
@@ -1594,10 +1600,40 @@ const collectReusableModelResources = (object: THREE.Object3D | null): ReusableM
|
||||
return { geometries, materials, textures }
|
||||
}
|
||||
|
||||
const mergeReusableResources = (...resources: ReusableModelResources[]) => {
|
||||
const merged: ReusableModelResources = {
|
||||
geometries: new Set(),
|
||||
materials: new Set(),
|
||||
textures: new Set()
|
||||
}
|
||||
|
||||
resources.forEach((resource) => {
|
||||
resource.geometries.forEach((geometry) => merged.geometries.add(geometry))
|
||||
resource.materials.forEach((material) => merged.materials.add(material))
|
||||
resource.textures.forEach((texture) => merged.textures.add(texture))
|
||||
})
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
const toReusableModelResources = (resources: GuideModelResourceSet): ReusableModelResources => ({
|
||||
geometries: resources.geometries,
|
||||
materials: resources.materials,
|
||||
textures: resources.textures
|
||||
})
|
||||
|
||||
const collectCachedModelResources = () => {
|
||||
return toReusableModelResources(guideModelLoadManager.collectProtectedResources())
|
||||
}
|
||||
|
||||
const disposeObject = (
|
||||
object: THREE.Object3D,
|
||||
protectedResources = collectReusableModelResources(null)
|
||||
protectedResources = collectReusableModelResources(null),
|
||||
protectCachedModelResources = true
|
||||
) => {
|
||||
const effectiveProtectedResources = protectCachedModelResources
|
||||
? mergeReusableResources(protectedResources, collectCachedModelResources())
|
||||
: protectedResources
|
||||
const disposedGeometries = new Set<THREE.BufferGeometry>()
|
||||
const disposedMaterials = new Set<THREE.Material>()
|
||||
const disposedTextures = new Set<THREE.Texture>()
|
||||
@@ -1606,7 +1642,7 @@ const disposeObject = (
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (
|
||||
child.geometry
|
||||
&& !protectedResources.geometries.has(child.geometry)
|
||||
&& !effectiveProtectedResources.geometries.has(child.geometry)
|
||||
&& !disposedGeometries.has(child.geometry)
|
||||
) {
|
||||
child.geometry.dispose()
|
||||
@@ -1614,8 +1650,8 @@ const disposeObject = (
|
||||
}
|
||||
const materials = Array.isArray(child.material) ? child.material : [child.material]
|
||||
materials.forEach((material) => {
|
||||
if (material && !protectedResources.materials.has(material) && !disposedMaterials.has(material)) {
|
||||
disposeMaterialTextures(material, protectedResources, disposedTextures)
|
||||
if (material && !effectiveProtectedResources.materials.has(material) && !disposedMaterials.has(material)) {
|
||||
disposeMaterialTextures(material, effectiveProtectedResources, disposedTextures)
|
||||
material.dispose()
|
||||
disposedMaterials.add(material)
|
||||
}
|
||||
@@ -1623,8 +1659,8 @@ const disposeObject = (
|
||||
}
|
||||
|
||||
if (child instanceof THREE.Sprite) {
|
||||
if (!protectedResources.materials.has(child.material) && !disposedMaterials.has(child.material)) {
|
||||
disposeMaterialTextures(child.material, protectedResources, disposedTextures)
|
||||
if (!effectiveProtectedResources.materials.has(child.material) && !disposedMaterials.has(child.material)) {
|
||||
disposeMaterialTextures(child.material, effectiveProtectedResources, disposedTextures)
|
||||
child.material.dispose()
|
||||
disposedMaterials.add(child.material)
|
||||
}
|
||||
@@ -1633,7 +1669,7 @@ const disposeObject = (
|
||||
if (child instanceof THREE.Line) {
|
||||
if (
|
||||
child.geometry
|
||||
&& !protectedResources.geometries.has(child.geometry)
|
||||
&& !effectiveProtectedResources.geometries.has(child.geometry)
|
||||
&& !disposedGeometries.has(child.geometry)
|
||||
) {
|
||||
child.geometry.dispose()
|
||||
@@ -1641,8 +1677,8 @@ const disposeObject = (
|
||||
}
|
||||
const materials = Array.isArray(child.material) ? child.material : [child.material]
|
||||
materials.forEach((material) => {
|
||||
if (material && !protectedResources.materials.has(material) && !disposedMaterials.has(material)) {
|
||||
disposeMaterialTextures(material, protectedResources, disposedTextures)
|
||||
if (material && !effectiveProtectedResources.materials.has(material) && !disposedMaterials.has(material)) {
|
||||
disposeMaterialTextures(material, effectiveProtectedResources, disposedTextures)
|
||||
material.dispose()
|
||||
disposedMaterials.add(material)
|
||||
}
|
||||
@@ -2170,6 +2206,34 @@ const wait = (delayMs: number) => new Promise<void>((resolve) => {
|
||||
window.setTimeout(resolve, delayMs)
|
||||
})
|
||||
|
||||
const getErrorStatus = (error: unknown) => {
|
||||
const candidate = error as {
|
||||
status?: unknown
|
||||
target?: { status?: unknown }
|
||||
currentTarget?: { status?: unknown }
|
||||
} | null
|
||||
const status = candidate?.status ?? candidate?.target?.status ?? candidate?.currentTarget?.status
|
||||
return typeof status === 'number' && Number.isFinite(status) ? status : undefined
|
||||
}
|
||||
|
||||
const formatModelLoadError = (error: unknown, url: string) => {
|
||||
if (!url.trim()) return '模型 URL 缺失'
|
||||
|
||||
const status = getErrorStatus(error)
|
||||
if (status === 500) return '模型代理返回 500,请检查 /gis/sdk/minio 或 MinIO 代理层'
|
||||
if (status === 404) return '模型资源不存在或代理返回 404'
|
||||
if (status && status >= 400) return `模型资源请求失败(HTTP ${status})`
|
||||
|
||||
const rawMessage = error instanceof Error ? error.message : String(error || '')
|
||||
const message = rawMessage.toLowerCase()
|
||||
if (message.includes('timeout')) return '模型资源请求超时'
|
||||
if (message.includes('parse') || message.includes('json') || message.includes('gltf')) {
|
||||
return '模型资源解析失败'
|
||||
}
|
||||
|
||||
return rawMessage || '模型资源加载失败'
|
||||
}
|
||||
|
||||
const loadModelOnce = (
|
||||
url: string,
|
||||
label: string,
|
||||
@@ -2198,13 +2262,18 @@ const loadModelOnce = (
|
||||
)
|
||||
})
|
||||
|
||||
const loadModel = async (
|
||||
const loadModelFromNetwork = async (
|
||||
url: string,
|
||||
label: string,
|
||||
loadToken?: number,
|
||||
options: LoadModelOptions = {}
|
||||
) => {
|
||||
let lastError: unknown = null
|
||||
const normalizedUrl = url.trim()
|
||||
|
||||
if (!normalizedUrl) {
|
||||
throw new Error('模型 URL 缺失')
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt <= modelLoadRetryDelaysMs.length; attempt += 1) {
|
||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
||||
@@ -2212,7 +2281,7 @@ const loadModel = async (
|
||||
}
|
||||
|
||||
try {
|
||||
return await loadModelOnce(url, label, loadToken, options)
|
||||
return await loadModelOnce(normalizedUrl, label, loadToken, options)
|
||||
} catch (error) {
|
||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
||||
throw createStaleModelLoadError()
|
||||
@@ -2230,7 +2299,33 @@ const loadModel = async (
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError instanceof Error ? lastError : new Error('模型资源加载失败')
|
||||
throw new Error(formatModelLoadError(lastError, normalizedUrl))
|
||||
}
|
||||
|
||||
const loadModelWithFallback = async (
|
||||
urls: string[],
|
||||
label: string,
|
||||
loadToken?: number,
|
||||
options: LoadModelOptions = {},
|
||||
semanticKey?: string
|
||||
) => {
|
||||
const candidates = urls.map((url) => url.trim()).filter(Boolean)
|
||||
if (!candidates.length) throw new Error('模型 URL 缺失')
|
||||
|
||||
const gltf = await guideModelLoadManager.load({
|
||||
urls: candidates,
|
||||
label,
|
||||
semanticKey,
|
||||
loadSource: (url) => loadModelFromNetwork(url, label, loadToken, options),
|
||||
shouldStopOnError: isStaleModelLoadError
|
||||
})
|
||||
|
||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
|
||||
disposeObject(gltf.scene)
|
||||
throw createStaleModelLoadError()
|
||||
}
|
||||
|
||||
return gltf
|
||||
}
|
||||
|
||||
const prepareModel = (model: THREE.Object3D) => {
|
||||
@@ -2295,6 +2390,67 @@ const canLoadFloorSilently = (floorId: string) => {
|
||||
return Boolean(floor && canAttachCachedSharedModel(floor.modelUrl))
|
||||
}
|
||||
|
||||
const getFloorModelUrls = (floor: FloorIndexItem) => (
|
||||
floor.modelUrls?.length ? floor.modelUrls : [floor.modelUrl]
|
||||
)
|
||||
|
||||
const getOverviewModelUrls = (packageData: GuideModelRenderPackage) => (
|
||||
packageData.overviewModelUrls?.length ? packageData.overviewModelUrls : [packageData.overviewModelUrl]
|
||||
)
|
||||
|
||||
const preloadModelUrls = async (urls: string[], label: string, preloadSeq: number) => {
|
||||
const candidates = urls.map((url) => url.trim()).filter(Boolean)
|
||||
if (!candidates.length || preloadSeq !== adjacentPreloadSeq || isDisposed) return
|
||||
|
||||
try {
|
||||
await guideModelLoadManager.preload({
|
||||
urls: candidates,
|
||||
label,
|
||||
loadSource: (url) => loadModelFromNetwork(url, label, undefined, { suppressProgress: true })
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('[ThreeMap] 相邻楼层模型预加载失败:', {
|
||||
urls: candidates,
|
||||
label,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getAdjacentFloors = (floorId: string) => {
|
||||
const orderedFloors = [...floorIndex.value].sort(compareFloorsTopToBottom)
|
||||
const currentIndex = orderedFloors.findIndex((floor) => floor.floorId === floorId)
|
||||
if (currentIndex < 0) return []
|
||||
|
||||
return [
|
||||
orderedFloors[currentIndex - 1],
|
||||
orderedFloors[currentIndex + 1]
|
||||
].filter((floor): floor is FloorIndexItem => Boolean(floor?.modelUrl))
|
||||
}
|
||||
|
||||
const scheduleAdjacentFloorPreload = (floorId: string) => {
|
||||
const adjacentFloors = getAdjacentFloors(floorId)
|
||||
if (!adjacentFloors.length) return
|
||||
|
||||
const preloadSeq = adjacentPreloadSeq + 1
|
||||
adjacentPreloadSeq = preloadSeq
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (preloadSeq !== adjacentPreloadSeq || isDisposed) return
|
||||
|
||||
void (async () => {
|
||||
for (const floor of adjacentFloors) {
|
||||
if (preloadSeq !== adjacentPreloadSeq || isDisposed) return
|
||||
await preloadModelUrls(
|
||||
getFloorModelUrls(floor),
|
||||
`预加载 ${formatFloorLabel(floor.floorId)} 模型`,
|
||||
preloadSeq
|
||||
)
|
||||
}
|
||||
})()
|
||||
}, adjacentPreloadDelayMs)
|
||||
}
|
||||
|
||||
const getFloorMatchKeys = (floor: FloorIndexItem) => (
|
||||
[floor.floorId, floor.label, ...(floor.modelMatchKeys || [])]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
@@ -2894,7 +3050,7 @@ const loadOverview = async () => {
|
||||
|
||||
setProgress(18, '正在加载建筑外观模型...')
|
||||
|
||||
const gltf = await loadModel(packageData.overviewModelUrl, '正在加载建筑外观模型', loadToken)
|
||||
const gltf = await loadModelWithFallback(getOverviewModelUrls(packageData), '正在加载建筑外观模型', loadToken)
|
||||
assertCurrentModelLoad(loadToken, gltf.scene)
|
||||
|
||||
const targetScene = scene
|
||||
@@ -2946,8 +3102,8 @@ const prepareFloorScene = async (
|
||||
setProgress(18, `正在加载 ${formatFloorLabel(requestedFloorId)} 模型...`)
|
||||
}
|
||||
|
||||
const gltf = await loadModel(
|
||||
floor.modelUrl,
|
||||
const gltf = await loadModelWithFallback(
|
||||
getFloorModelUrls(floor),
|
||||
`正在加载 ${formatFloorLabel(requestedFloorId)} 模型`,
|
||||
loadToken,
|
||||
{ suppressProgress: options.suppressProgress }
|
||||
@@ -3046,6 +3202,7 @@ const commitPreparedFloorScene = (
|
||||
markFloorAutoSwitchEntry()
|
||||
refreshPoiVisibilityByDistance()
|
||||
renderRoutePreview()
|
||||
scheduleAdjacentFloorPreload(expectedFloorId)
|
||||
}
|
||||
|
||||
const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
|
||||
@@ -3106,7 +3263,7 @@ const loadMultiFloor = async () => {
|
||||
let sourceModel = cachedSharedModelUrl === sharedModelUrl ? cachedOverviewModel : null
|
||||
|
||||
if (!sourceModel) {
|
||||
const gltf = await loadModel(sharedModelUrl, '正在加载多层共享模型', loadToken)
|
||||
const gltf = await loadModelWithFallback([sharedModelUrl], '正在加载多层共享模型', loadToken)
|
||||
assertCurrentModelLoad(loadToken, gltf.scene)
|
||||
|
||||
const targetScene = scene
|
||||
@@ -3178,7 +3335,7 @@ const loadMultiFloor = async () => {
|
||||
|
||||
const floor = orderedFloors[index]
|
||||
const label = formatFloorLabel(floor.floorId)
|
||||
const gltf = await loadModel(floor.modelUrl, `正在加载 ${label} 多层模型`, loadToken)
|
||||
const gltf = await loadModelWithFallback(getFloorModelUrls(floor), `正在加载 ${label} 多层模型`, loadToken)
|
||||
if (!isCurrentModelLoad(loadToken)) {
|
||||
disposeObject(gltf.scene)
|
||||
disposeObject(group)
|
||||
@@ -4259,6 +4416,7 @@ const retryLoad = () => {
|
||||
|
||||
const disposeScene = () => {
|
||||
isDisposed = true
|
||||
adjacentPreloadSeq += 1
|
||||
invalidateModelLoads()
|
||||
|
||||
if (animationId) {
|
||||
|
||||
Reference in New Issue
Block a user