优化三维导览模型加载与交互体验

This commit is contained in:
lyf
2026-06-27 23:49:38 +08:00
parent 4f6059bbdb
commit f043c85af0
12 changed files with 1433 additions and 509 deletions

View File

@@ -124,6 +124,19 @@ interface MultiFloorModelItem {
size: THREE.Vector3
}
interface ReusableModelResources {
geometries: Set<THREE.BufferGeometry>
materials: Set<THREE.Material>
textures: Set<THREE.Texture>
}
interface PoiMarkerCacheEntry {
floorId: string
pois: RenderPoi[]
group: THREE.Group
markerSize: number
}
type ContainerRef = HTMLElement | { $el?: HTMLElement } | null
const props = withDefaults(defineProps<{
@@ -154,8 +167,8 @@ const props = withDefaults(defineProps<{
routePreview: null,
showRoute: false,
autoSwitch: true,
autoSwitchThresholdLow: 1.0,
autoSwitchThresholdHigh: 1.3,
autoSwitchThresholdLow: 1.45,
autoSwitchThresholdHigh: 3.8,
autoSwitchCooldown: 2000
})
@@ -199,6 +212,9 @@ let dracoLoader: DRACOLoader | null = null
let activeModel: THREE.Object3D | null = null
let poiGroup: THREE.Group | null = null
let routeGroup: THREE.Group | null = null
let activeRoutePreviewSignature = ''
const poiDataCache = new Map<string, RenderPoi[]>()
const poiMarkerGroupCache = new Map<string, PoiMarkerCacheEntry>()
let activeFocusLabelSprite: THREE.Sprite | null = null
let activeFocusPulseSprite: THREE.Sprite | null = null
let activeFocusBaseSprite: THREE.Sprite | null = null
@@ -218,11 +234,20 @@ let autoSwitchOverviewMaxDim = 0
// 自动切换状态
let isAutoSwitchLocked = false
let lastAutoSwitchTime = 0
let lastControlDistance = 0
let floorAutoSwitchProtectedUntil = 0
let floorExitThresholdExceededSince = 0
let floorExitZoomOutAccumulatedDistance = 0
let autoSwitchTemporarilyDisabled = false
let autoSwitchDisableTimer: ReturnType<typeof setTimeout> | null = null
let isProgrammaticCameraChange = false
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
const floorAutoSwitchProtectionMs = 1600
const floorExitZoomOutIntentMinDeltaRatio = 0.01
const floorExitZoomOutAccumulatedRatio = 0.12
const floorExitThresholdHoldMs = 650
const syncControlInteractionOptions = () => {
if (!controls) return
@@ -549,8 +574,7 @@ const updatePoiVisibilityByDistance = () => {
const acceptedPositions: THREE.Vector2[] = []
let visibleCount = 0
const candidates = poiGroup.children
.filter((child): child is THREE.Sprite => child instanceof THREE.Sprite && !isPoiAuxiliarySprite(child))
const candidates = getPoiSprites()
.map((sprite) => {
const poi = sprite.userData.poi as RenderPoi | undefined
const screenPosition = sprite.position.clone().project(camera!)
@@ -744,27 +768,195 @@ const handleResize = () => {
renderer.setSize(width, height)
}
const disposeObject = (object: THREE.Object3D) => {
const materialTextureKeys = [
'map',
'alphaMap',
'aoMap',
'bumpMap',
'normalMap',
'displacementMap',
'roughnessMap',
'metalnessMap',
'emissiveMap',
'specularMap',
'envMap',
'lightMap',
'gradientMap',
'matcap',
'clearcoatMap',
'clearcoatNormalMap',
'clearcoatRoughnessMap',
'sheenColorMap',
'sheenRoughnessMap',
'transmissionMap',
'thicknessMap',
'iridescenceMap',
'iridescenceThicknessMap'
] as const
const collectMaterialTextures = (material: THREE.Material | null | undefined) => {
const textures = new Set<THREE.Texture>()
if (!material) return textures
const materialRecord = material as unknown as Record<string, unknown>
materialTextureKeys.forEach((key) => {
const texture = materialRecord[key]
if (texture instanceof THREE.Texture) {
textures.add(texture)
}
})
return textures
}
const disposeMaterialTextures = (
material: THREE.Material,
protectedResources: ReusableModelResources,
disposedTextures: Set<THREE.Texture>
) => {
collectMaterialTextures(material).forEach((texture) => {
if (!protectedResources.textures.has(texture) && !disposedTextures.has(texture)) {
texture.dispose()
disposedTextures.add(texture)
}
})
}
const collectReusableModelResources = (object: THREE.Object3D | null): ReusableModelResources => {
const geometries = new Set<THREE.BufferGeometry>()
const materials = new Set<THREE.Material>()
const textures = new Set<THREE.Texture>()
object?.traverse((child) => {
if (child instanceof THREE.Mesh || child instanceof THREE.Line) {
if (child.geometry) {
geometries.add(child.geometry)
}
const childMaterials = Array.isArray(child.material) ? child.material : [child.material]
childMaterials.forEach((material) => {
if (!material) return
materials.add(material)
collectMaterialTextures(material).forEach((texture) => textures.add(texture))
})
}
if (child instanceof THREE.Sprite && child.material) {
materials.add(child.material)
collectMaterialTextures(child.material).forEach((texture) => textures.add(texture))
}
})
return { geometries, materials, textures }
}
const disposeObject = (
object: THREE.Object3D,
protectedResources = collectReusableModelResources(null)
) => {
const disposedGeometries = new Set<THREE.BufferGeometry>()
const disposedMaterials = new Set<THREE.Material>()
const disposedTextures = new Set<THREE.Texture>()
object.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.geometry?.dispose()
if (
child.geometry
&& !protectedResources.geometries.has(child.geometry)
&& !disposedGeometries.has(child.geometry)
) {
child.geometry.dispose()
disposedGeometries.add(child.geometry)
}
const materials = Array.isArray(child.material) ? child.material : [child.material]
materials.forEach((material) => material?.dispose())
materials.forEach((material) => {
if (material && !protectedResources.materials.has(material) && !disposedMaterials.has(material)) {
disposeMaterialTextures(material, protectedResources, disposedTextures)
material.dispose()
disposedMaterials.add(material)
}
})
}
if (child instanceof THREE.Sprite) {
child.material.map?.dispose()
child.material.dispose()
if (!protectedResources.materials.has(child.material) && !disposedMaterials.has(child.material)) {
disposeMaterialTextures(child.material, protectedResources, disposedTextures)
child.material.dispose()
disposedMaterials.add(child.material)
}
}
if (child instanceof THREE.Line) {
child.geometry?.dispose()
if (
child.geometry
&& !protectedResources.geometries.has(child.geometry)
&& !disposedGeometries.has(child.geometry)
) {
child.geometry.dispose()
disposedGeometries.add(child.geometry)
}
const materials = Array.isArray(child.material) ? child.material : [child.material]
materials.forEach((material) => material?.dispose())
materials.forEach((material) => {
if (material && !protectedResources.materials.has(material) && !disposedMaterials.has(material)) {
disposeMaterialTextures(material, protectedResources, disposedTextures)
material.dispose()
disposedMaterials.add(material)
}
})
}
})
}
const isPoiMarkerSprite = (child: THREE.Object3D): child is THREE.Sprite => (
child instanceof THREE.Sprite
&& Boolean(child.userData.poi)
&& !isPoiAuxiliarySprite(child)
)
const getPoiSprites = () => {
const sprites: THREE.Sprite[] = []
poiGroup?.traverse((child) => {
if (isPoiMarkerSprite(child)) {
sprites.push(child)
}
})
return sprites
}
const detachPoiMarkerGroups = () => {
poiMarkerGroupCache.forEach((entry) => {
entry.group.parent?.remove(entry.group)
})
}
const disposePoiMarkerCache = () => {
detachPoiMarkerGroups()
poiMarkerGroupCache.forEach((entry) => {
disposeObject(entry.group)
})
poiMarkerGroupCache.clear()
poiDataCache.clear()
}
const clearPoiGroupChildren = () => {
if (!poiGroup) return
while (poiGroup.children.length) {
const child = poiGroup.children[0]
poiGroup.remove(child)
if (child instanceof THREE.Group) {
const cachedEntry = [...poiMarkerGroupCache.values()].find((entry) => entry.group === child)
if (cachedEntry) continue
}
disposeObject(child)
}
}
const getMaterialColor = (material: THREE.Material) => {
const candidate = material as THREE.Material & { color?: unknown }
return candidate.color instanceof THREE.Color ? candidate.color : undefined
@@ -943,6 +1135,7 @@ const disposeCachedOverviewModel = () => {
}
const clearRoutePreview = () => {
activeRoutePreviewSignature = ''
if (!routeGroup) return
while (routeGroup.children.length) {
@@ -1073,15 +1266,54 @@ const renderRouteConnectorMarkers = (route: GuideRouteResult, markerSize: number
})
}
const renderRoutePreview = () => {
clearRoutePreview()
const routePositionSignature = (position: [number, number, number]) => (
position.map((value) => Number(value.toFixed(3))).join(',')
)
const getRoutePreviewSignature = (
route: GuideRouteResult,
visibleSegments: GuideRouteFloorSegment[],
markerSize: number
) => JSON.stringify({
routeId: route.id,
view: activeView.value,
floor: activeView.value === 'floor' ? currentFloor.value : '',
markerSize: Number(markerSize.toFixed(3)),
start: {
poiId: route.start.poiId,
floorId: route.start.floorId,
position: routePositionSignature(route.start.position)
},
end: {
poiId: route.end.poiId,
floorId: route.end.floorId,
position: routePositionSignature(route.end.position)
},
segments: visibleSegments.map((segment) => ({
floorId: segment.floorId,
points: segment.points.map((point) => `${point.nodeId}:${routePositionSignature(point.position)}`)
})),
connectors: route.connectorPoints
.filter((point) => activeView.value !== 'floor' || point.floorId === currentFloor.value)
.map((point) => `${point.nodeId}:${point.floorId}:${routePositionSignature(point.position)}`)
})
const renderRoutePreview = () => {
if (!props.showRoute || !props.routePreview || !routeGroup || !scene) {
clearRoutePreview()
return
}
if (!props.showRoute || !props.routePreview || !routeGroup || !scene) return
const route = props.routePreview
const targetRouteGroup = routeGroup
const visibleSegments = getVisibleRouteSegments(route)
const markerSize = Math.max(getPoiMarkerSize() * 0.82, 2.8)
const signature = getRoutePreviewSignature(route, visibleSegments, markerSize)
if (signature === activeRoutePreviewSignature) return
clearRoutePreview()
activeRoutePreviewSignature = signature
visibleSegments.forEach((segment) => {
if (segment.points.length < 2) return
@@ -1104,7 +1336,7 @@ const clearSceneData = () => {
if (activeModel === cachedOverviewModel) {
cachedOverviewModel.visible = false
} else {
disposeObject(activeModel)
disposeObject(activeModel, collectReusableModelResources(cachedOverviewModel))
}
}
@@ -1114,14 +1346,7 @@ const clearSceneData = () => {
disposeFocusBase()
selectedPOI.value = null
clearRoutePreview()
if (poiGroup) {
while (poiGroup.children.length) {
const child = poiGroup.children[0]
poiGroup.remove(child)
disposeObject(child)
}
}
clearPoiGroupChildren()
}
const loadModel = (url: string, label: string, loadToken?: number) => new Promise<GLTF>((resolve, reject) => {
@@ -1191,6 +1416,63 @@ const applyMultiFloorLayout = (items: MultiFloorModelItem[]) => {
})
}
const getSharedMultiFloorModelUrl = (floorsToLoad: FloorIndexItem[]) => {
if (!floorsToLoad.length || !floorsToLoad.every((floor) => floor.sharedModelAsset)) return ''
const [firstFloor] = floorsToLoad
return floorsToLoad.every((floor) => floor.modelUrl === firstFloor.modelUrl)
? firstFloor.modelUrl
: ''
}
const createMultiFloorItemFromSharedModel = (sourceModel: THREE.Object3D, floor: FloorIndexItem) => {
const floorModel = sourceModel.clone(true)
floorModel.name = `GuideMultiFloorModel_${floor.floorId}`
floorModel.userData.floorId = floor.floorId
prepareModel(floorModel)
applyModelVisibilityForView(floorModel, 'floor', floor.floorId)
return {
floor,
label: formatFloorLabel(floor.floorId),
model: floorModel,
size: getObjectSize(floorModel)
}
}
const disposeDetachedMultiFloorModels = (
items: MultiFloorModelItem[],
group: THREE.Group,
protectedResources = collectReusableModelResources(null)
) => {
items
.filter((item) => !group.children.includes(item.model))
.forEach((item) => disposeObject(item.model, protectedResources))
}
const updateLastControlDistance = () => {
if (!controls) return
const distance = controls.getDistance()
if (Number.isFinite(distance)) {
lastControlDistance = distance
}
}
const markFloorAutoSwitchEntry = () => {
floorAutoSwitchProtectedUntil = Date.now() + floorAutoSwitchProtectionMs
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
updateLastControlDistance()
}
const resetAutoSwitchDistanceTracking = () => {
floorAutoSwitchProtectedUntil = 0
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
updateLastControlDistance()
}
const checkAutoSwitch = () => {
// 检查是否启用自动切换
if (!props.autoSwitch || autoSwitchTemporarilyDisabled || isProgrammaticCameraChange || isLoading.value) {
@@ -1215,18 +1497,23 @@ const checkAutoSwitch = () => {
// 获取当前距离
const distance = controls.getDistance()
const previousDistance = lastControlDistance || distance
const distanceDelta = distance - previousDistance
lastControlDistance = distance
// 自动切换使用全馆稳定尺寸,避免单楼层可见性过滤压低 floor -> overview 阈值。
const maxDim = autoSwitchOverviewMaxDim || getObjectMaxDim(activeModel)
const thresholdLow = maxDim * props.autoSwitchThresholdLow
const thresholdHigh = maxDim * props.autoSwitchThresholdHigh
const enterFloorAt = maxDim * props.autoSwitchThresholdLow
const exitFloorAt = maxDim * props.autoSwitchThresholdHigh
// 判断是否需要切换
if (activeView.value === 'overview' && distance < thresholdLow) {
if (activeView.value === 'overview' && distance < enterFloorAt) {
// 从建筑外观自动切换到单楼层
isAutoSwitchLocked = true
lastAutoSwitchTime = now
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
void runAutoSwitchLoad(
{
@@ -1238,10 +1525,52 @@ const checkAutoSwitch = () => {
() => loadFloor(currentFloor.value || props.initialFloorId || 'L1'),
'单楼层模型加载失败'
)
} else if (activeView.value === 'floor' && !props.targetFocus && distance > thresholdHigh) {
return
}
if (activeView.value !== 'floor' || props.targetFocus) {
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
return
}
if (now < floorAutoSwitchProtectedUntil) {
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
return
}
const zoomOutIntentDelta = Math.max(maxDim * floorExitZoomOutIntentMinDeltaRatio, 1)
const isClearZoomOutIntent = distanceDelta > zoomOutIntentDelta
const isBeyondExitThreshold = distance > exitFloorAt
if (!isBeyondExitThreshold) {
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
return
}
if (isClearZoomOutIntent) {
floorExitZoomOutAccumulatedDistance += distanceDelta
if (!floorExitThresholdExceededSince) {
floorExitThresholdExceededSince = now
}
}
const requiredZoomOutDistance = Math.max(maxDim * floorExitZoomOutAccumulatedRatio, 8)
if (
!floorExitThresholdExceededSince
|| floorExitZoomOutAccumulatedDistance < requiredZoomOutDistance
) {
return
}
if (now - floorExitThresholdExceededSince >= floorExitThresholdHoldMs) {
// 从单楼层自动切换到完整外围模型
isAutoSwitchLocked = true
lastAutoSwitchTime = now
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
void runAutoSwitchLoad(
{
@@ -1362,10 +1691,13 @@ const zoomCamera = (direction: 'in' | 'out') => {
offset.setLength(nextDistance)
camera.position.copy(controls.target).add(offset)
controls.update()
lastControlDistance = nextDistance
if (direction === 'in' && activeView.value === 'overview' && !isLoading.value && !isAutoSwitchLocked) {
isAutoSwitchLocked = true
lastAutoSwitchTime = Date.now()
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
void runAutoSwitchLoad(
{
@@ -1401,6 +1733,7 @@ const loadOverview = async () => {
cacheAutoSwitchOverviewMaxDim(activeModel)
targetScene.add(activeModel)
fitCameraToObject(activeModel)
resetAutoSwitchDistanceTracking()
refreshPoiVisibilityByDistance()
renderRoutePreview()
return
@@ -1426,6 +1759,7 @@ const loadOverview = async () => {
cachedSharedModelUrl = packageData.overviewModelUrl
targetScene.add(activeModel)
fitCameraToObject(activeModel)
resetAutoSwitchDistanceTracking()
refreshPoiVisibilityByDistance()
renderRoutePreview()
}
@@ -1486,6 +1820,7 @@ const loadFloor = async (floorId: string) => {
if (!activeModel) throw createStaleModelLoadError()
fitCameraToObject(activeModel)
markFloorAutoSwitchEntry()
if (shouldRenderPoiMarkers.value) {
await loadFloorPOIs(floor, loadToken)
@@ -1508,9 +1843,81 @@ const loadMultiFloor = async () => {
group.name = 'GuideMultiFloorModel'
const orderedFloors = [...floorIndex.value].sort((a, b) => a.order - b.order)
const loadedFloors: MultiFloorModelItem[] = []
const sharedModelUrl = getSharedMultiFloorModelUrl(orderedFloors)
if (sharedModelUrl) {
let sourceModel = cachedSharedModelUrl === sharedModelUrl ? cachedOverviewModel : null
if (!sourceModel) {
const gltf = await loadModel(sharedModelUrl, '正在加载多层共享模型', loadToken)
assertCurrentModelLoad(loadToken, gltf.scene)
const targetScene = scene
if (!targetScene) {
disposeObject(gltf.scene)
disposeObject(group)
throw createStaleModelLoadError()
}
sourceModel = gltf.scene
sourceModel.name = 'GuideOverviewModel'
prepareModel(sourceModel)
applyModelVisibilityForView(sourceModel, 'overview')
cacheAutoSwitchOverviewMaxDim(sourceModel)
cachedOverviewModel = sourceModel
cachedSharedModelUrl = sharedModelUrl
}
const sharedSourceModel = sourceModel
const protectedResources = collectReusableModelResources(sharedSourceModel)
try {
orderedFloors.forEach((floor, index) => {
assertCurrentModelLoad(loadToken, group)
const item = createMultiFloorItemFromSharedModel(sharedSourceModel, floor)
loadedFloors.push(item)
setProgress(
18 + Math.round(((index + 1) / orderedFloors.length) * 72),
`已准备 ${item.label}`
)
})
assertCurrentModelLoad(loadToken, group)
applyMultiFloorLayout(loadedFloors)
loadedFloors.forEach((item) => group.add(item.model))
const targetScene = scene
if (!targetScene) {
disposeObject(group, protectedResources)
throw createStaleModelLoadError()
}
activeModel = group
targetScene.add(activeModel)
fitCameraToObject(activeModel)
resetAutoSwitchDistanceTracking()
refreshPoiVisibilityByDistance()
renderRoutePreview()
return
} catch (error) {
disposeObject(group, protectedResources)
disposeDetachedMultiFloorModels(loadedFloors, group, protectedResources)
if (isStaleModelLoadError(error)) {
throw error
}
throw error
}
}
for (let index = 0; index < orderedFloors.length; index += 1) {
assertCurrentModelLoad(loadToken, group)
try {
assertCurrentModelLoad(loadToken, group)
} catch (error) {
disposeDetachedMultiFloorModels(loadedFloors, group)
throw error
}
const floor = orderedFloors[index]
const label = formatFloorLabel(floor.floorId)
@@ -1518,6 +1925,7 @@ const loadMultiFloor = async () => {
if (!isCurrentModelLoad(loadToken)) {
disposeObject(gltf.scene)
disposeObject(group)
disposeDetachedMultiFloorModels(loadedFloors, group)
throw createStaleModelLoadError()
}
@@ -1538,19 +1946,27 @@ const loadMultiFloor = async () => {
)
}
assertCurrentModelLoad(loadToken, group)
try {
assertCurrentModelLoad(loadToken, group)
} catch (error) {
disposeDetachedMultiFloorModels(loadedFloors, group)
throw error
}
applyMultiFloorLayout(loadedFloors)
loadedFloors.forEach((item) => group.add(item.model))
const targetScene = scene
if (!targetScene) {
disposeObject(group)
disposeDetachedMultiFloorModels(loadedFloors, group)
throw createStaleModelLoadError()
}
activeModel = group
targetScene.add(activeModel)
fitCameraToObject(activeModel)
resetAutoSwitchDistanceTracking()
refreshPoiVisibilityByDistance()
renderRoutePreview()
}
@@ -1873,25 +2289,16 @@ const setPoiSpriteFocusStyle = (sprite: THREE.Sprite, focused: boolean) => {
const updatePoiMarkerFocus = () => {
if (!poiGroup) return
poiGroup.children.forEach((child) => {
if (
child instanceof THREE.Sprite
&& !child.userData.isPoiLabel
&& !child.userData.isPoiPulse
&& !child.userData.isPoiBase
) {
const poi = child.userData.poi as RenderPoi | undefined
setPoiSpriteFocusStyle(child, Boolean(poi && poi.id === activeFocusPoiId.value))
}
getPoiSprites().forEach((sprite) => {
const poi = sprite.userData.poi as RenderPoi | undefined
setPoiSpriteFocusStyle(sprite, Boolean(poi && poi.id === activeFocusPoiId.value))
})
refreshPoiVisibilityByDistance()
}
const findPoiSprite = (poiId: string) => (
poiGroup?.children.find((child): child is THREE.Sprite => (
child instanceof THREE.Sprite && (child.userData.poi as RenderPoi | undefined)?.id === poiId
))
getPoiSprites().find((sprite) => (sprite.userData.poi as RenderPoi | undefined)?.id === poiId)
)
const findLoadedPoi = (poiId: string) => (
@@ -1932,19 +2339,12 @@ const addFocusMarkerFromRequest = (request: TargetPoiFocusRequest) => {
return poi
}
const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
if (!poiGroup) return
const createPoiMarkerGroup = (floorId: string, pois: RenderPoi[], markerSize: number) => {
const group = new THREE.Group()
group.name = `GuideModelPOI_${floorId}`
group.userData.floorId = floorId
const validPois = (await props.modelSource.loadFloorPois(floor.floorId))
.filter((poi) => shouldShowPoiInCurrentMode(poi))
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) return
const targetPoiGroup = poiGroup
if (!targetPoiGroup) return
const markerSize = getPoiMarkerSize()
validPois.forEach((poi) => {
pois.forEach((poi) => {
const [x, y, z] = poi.positionGltf!
const sprite = new THREE.Sprite(createPoiMaterial(poi))
sprite.position.set(x, y + markerSize * 0.5, z)
@@ -1953,8 +2353,55 @@ const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
sprite.userData.isCorePoi = corePoiCategories.has(poi.primaryCategory)
setPoiSpriteFocusStyle(sprite, poi.id === activeFocusPoiId.value)
sprite.visible = shouldShowPoiAtDistance(poi, getPoiVisibilityTier())
targetPoiGroup.add(sprite)
group.add(sprite)
})
return group
}
const attachPoiMarkerGroup = (entry: PoiMarkerCacheEntry) => {
if (!poiGroup) return
detachPoiMarkerGroups()
if (entry.group.parent !== poiGroup) {
poiGroup.add(entry.group)
}
updatePoiMarkerFocus()
}
const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
if (!poiGroup) return
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) return
const cachedEntry = poiMarkerGroupCache.get(floor.floorId)
if (cachedEntry) {
attachPoiMarkerGroup(cachedEntry)
return
}
const cachedPois = poiDataCache.get(floor.floorId)
const validPois = cachedPois || (await props.modelSource.loadFloorPois(floor.floorId))
.filter((poi) => shouldShowPoiInCurrentMode(poi))
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) return
poiDataCache.set(floor.floorId, validPois)
const markerSize = getPoiMarkerSize()
const markerGroup = createPoiMarkerGroup(floor.floorId, validPois, markerSize)
const entry: PoiMarkerCacheEntry = {
floorId: floor.floorId,
pois: validPois,
group: markerGroup,
markerSize
}
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) {
disposeObject(markerGroup)
return
}
poiMarkerGroupCache.set(floor.floorId, entry)
attachPoiMarkerGroup(entry)
}
const clearMapSelection = (shouldEmit = true) => {
@@ -1997,7 +2444,7 @@ const handleSceneTap = (event: PointerEvent) => {
raycaster.setFromCamera(pointer, camera)
if (activeView.value === 'floor' && poiGroup) {
const poiHits = raycaster.intersectObjects(poiGroup.children, false)
const poiHits = raycaster.intersectObjects(getPoiSprites(), false)
const hit = poiHits.find((item) => (
item.object.visible
&& item.object instanceof THREE.Sprite
@@ -2126,7 +2573,7 @@ const focusTargetPoi = async (request: TargetPoiFocusRequest) => {
await loadFloor(request.floorId)
isLoading.value = false
emit('floorChange', request.floorId)
} else if (shouldRenderPoiMarkers.value && poiGroup && poiGroup.children.length === 0) {
} else if (shouldRenderPoiMarkers.value && !getPoiSprites().length) {
await loadFloorPOIs(floor, modelLoadVersion)
}
@@ -2311,8 +2758,13 @@ const disposeScene = () => {
programmaticCameraTimer = null
}
isProgrammaticCameraChange = false
lastControlDistance = 0
floorAutoSwitchProtectedUntil = 0
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
clearSceneData()
disposePoiMarkerCache()
disposeCachedOverviewModel()
scene?.clear()
scene = null