修复点位楼层跳转并优化模型加载缓存

This commit is contained in:
lyf
2026-07-02 16:42:38 +08:00
parent 7cda427de9
commit 9b1f855515
8 changed files with 750 additions and 41 deletions

View File

@@ -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) {