优化三维导览模型加载与交互体验
This commit is contained in:
@@ -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,25 +768,193 @@ 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()
|
||||
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) => {
|
||||
@@ -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) {
|
||||
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 () => {
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -193,6 +193,9 @@ import type {
|
||||
import type {
|
||||
GuideRouteResult
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
isIndoorNavigableFloor
|
||||
} from '@/domain/guideFloor'
|
||||
|
||||
interface GuideFloorOption {
|
||||
id: string
|
||||
@@ -367,7 +370,8 @@ const indoorRendererRef = ref<{
|
||||
disableAutoSwitchTemporarily?: (durationMs: number) => void
|
||||
} | null>(null)
|
||||
|
||||
const floorLabels = computed(() => props.floors.map((floor) => floor.label))
|
||||
const indoorFloors = computed(() => props.floors.filter((floor) => isIndoorNavigableFloor(floor)))
|
||||
const floorLabels = computed(() => indoorFloors.value.map((floor) => floor.label))
|
||||
// #ifdef H5
|
||||
const effectiveIndoorModelSource = computed(() => props.indoorModelSource)
|
||||
// #endif
|
||||
@@ -376,13 +380,13 @@ const showIndoorRightControls = computed(() => (
|
||||
))
|
||||
|
||||
const activeFloorId = computed(() => {
|
||||
const matchedFloor = props.floors.find((floor) => (
|
||||
const matchedFloor = indoorFloors.value.find((floor) => (
|
||||
floor.id === props.activeFloor || floor.label === props.activeFloor
|
||||
))
|
||||
if (matchedFloor) return matchedFloor.id
|
||||
|
||||
const normalizedFloorId = props.normalizeFloorId(props.activeFloor)
|
||||
const normalizedFloor = props.floors.find((floor) => (
|
||||
const normalizedFloor = indoorFloors.value.find((floor) => (
|
||||
floor.id === normalizedFloorId || floor.label === normalizedFloorId
|
||||
))
|
||||
return normalizedFloor?.id || normalizedFloorId
|
||||
@@ -497,7 +501,7 @@ const handleMoreTap = () => {
|
||||
}
|
||||
|
||||
const handleThreeFloorChange = (floorId: string) => {
|
||||
const floor = props.floors.find((item) => item.id === floorId)
|
||||
const floor = indoorFloors.value.find((item) => item.id === floorId)
|
||||
emit('floorChange', floor?.label || floorId)
|
||||
emit('indoorViewChange', 'floor')
|
||||
emit('layerModeChange', 'single')
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import {
|
||||
isIndoorNavigableFloor
|
||||
} from '@/domain/guideFloor'
|
||||
|
||||
export interface RoutePointOption {
|
||||
poiId: string
|
||||
@@ -128,9 +131,14 @@ watch(
|
||||
const normalizedKeyword = computed(() => keyword.value.trim().toLowerCase())
|
||||
|
||||
const filteredOptions = computed(() => {
|
||||
if (!normalizedKeyword.value) return props.options
|
||||
const indoorOptions = props.options.filter((option) => isIndoorNavigableFloor({
|
||||
floorId: option.floorId,
|
||||
label: option.floorLabel
|
||||
}))
|
||||
|
||||
return props.options.filter((option) => {
|
||||
if (!normalizedKeyword.value) return indoorOptions
|
||||
|
||||
return indoorOptions.filter((option) => {
|
||||
const fields = [
|
||||
option.name,
|
||||
option.floorId,
|
||||
|
||||
@@ -6,6 +6,9 @@ import type {
|
||||
StaticNavPoiCategoryPayload,
|
||||
StaticNavPoiPayload
|
||||
} from '@/data/providers/staticNavAssetsProvider'
|
||||
import {
|
||||
isIndoorNavigableFloor
|
||||
} from '@/domain/guideFloor'
|
||||
|
||||
const floorIdPattern = /^L(-?\d+(?:\.\d+)?)$/
|
||||
|
||||
@@ -31,6 +34,13 @@ export const navFloorIdFromLabel = (labelOrId: string) => {
|
||||
return labelOrId
|
||||
}
|
||||
|
||||
export const isStaticIndoorNavigableFloorId = (floorId: string) => (
|
||||
isIndoorNavigableFloor({
|
||||
floorId,
|
||||
label: formatNavFloorLabel(floorId)
|
||||
})
|
||||
)
|
||||
|
||||
const toCategory = (category: StaticNavPoiCategoryPayload): MuseumCategory => ({
|
||||
id: category.topCategory,
|
||||
label: category.topCategoryZh,
|
||||
|
||||
@@ -13,6 +13,9 @@ import type {
|
||||
import {
|
||||
NAV_ROUTE_UNAVAILABLE_MESSAGE
|
||||
} from '@/domain/guideReadiness'
|
||||
import {
|
||||
isIndoorNavigableFloor
|
||||
} from '@/domain/guideFloor'
|
||||
import type {
|
||||
SgsFloorDiagnosticsPayload,
|
||||
SgsMapDiagnosticsPayload,
|
||||
@@ -129,6 +132,8 @@ export const buildSgsFloorAliases = (floors: SgsSdkFloorSummaryPayload[]) => {
|
||||
const aliases = new Map<string, string>()
|
||||
|
||||
floors.forEach((floor) => {
|
||||
if (!isIndoorNavigableFloor(floor)) return
|
||||
|
||||
const floorId = stringifyId(floor.floorId)
|
||||
if (!floorId) return
|
||||
|
||||
@@ -242,16 +247,17 @@ export const toGuideMapDiagnostics = (
|
||||
const floors = diagnostics.floors?.length
|
||||
? diagnostics.floors.map(toGuideFloorDetailFromSgs)
|
||||
: (manifest?.floors || []).map(toGuideFloorDetailFromSgs)
|
||||
const indoorFloors = floors.filter(isIndoorNavigableFloor)
|
||||
|
||||
return {
|
||||
mapId: stringifyId(diagnostics.mapId || manifest?.mapId),
|
||||
mapName: diagnostics.mapName || manifest?.mapName || 'SGS 地图',
|
||||
sdkVersion: diagnostics.sdkVersion || manifest?.sdkVersion || undefined,
|
||||
status: normalizeStatus(diagnostics.status),
|
||||
floors,
|
||||
floors: indoorFloors,
|
||||
summary: {
|
||||
floorCount: toNumber(summary.floorCount, floors.length),
|
||||
modelReadyFloorCount: toNumber(summary.modelReadyFloorCount),
|
||||
floorCount: indoorFloors.length,
|
||||
modelReadyFloorCount: indoorFloors.filter((floor) => floor.modelReady === true).length || toNumber(summary.modelReadyFloorCount),
|
||||
poiCount: toNumber(summary.poiCount),
|
||||
spaceCount: toNumber(summary.spaceCount),
|
||||
guideStopCount: toNumber(summary.guideStopCount),
|
||||
@@ -267,7 +273,7 @@ export const toRouteReadinessFromSgsDiagnostics = (
|
||||
diagnostics: SgsMapDiagnosticsPayload
|
||||
): GuideRouteReadiness => {
|
||||
const warnings = diagnostics.warnings || []
|
||||
const floors = diagnostics.floors || []
|
||||
const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor)
|
||||
const notReadyFloors = floors.filter((floor) => (
|
||||
floor.modelReady !== true
|
||||
|| floor.routePlanningReady !== true
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<text class="more-action-desc">{{ routePlannerEntryDesc }}</text>
|
||||
</view>
|
||||
<view class="more-action" @tap="handleMoreOutdoorNav">
|
||||
<text class="more-action-title">室外导航</text>
|
||||
<text class="more-action-title">来馆</text>
|
||||
<text class="more-action-desc">导航到博物馆</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -591,9 +591,8 @@ const buildExplainHallItems = (
|
||||
}
|
||||
|
||||
const handleGuideSearchTap = () => {
|
||||
uni.showToast({
|
||||
title: '功能待开放',
|
||||
icon: 'none'
|
||||
uni.navigateTo({
|
||||
url: '/pages/search/index'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,9 @@ import type {
|
||||
import {
|
||||
dataSourceConfig
|
||||
} from '@/config/dataSource'
|
||||
import {
|
||||
isIndoorNavigableFloor
|
||||
} from '@/domain/guideFloor'
|
||||
import {
|
||||
formatSgsFloorLabel,
|
||||
toMuseumPoiFromSgs
|
||||
@@ -17,7 +20,8 @@ import {
|
||||
type SgsSdkFloorSummaryPayload
|
||||
} from '@/data/providers/sgsSdkApiProvider'
|
||||
import {
|
||||
formatNavFloorLabel
|
||||
formatNavFloorLabel,
|
||||
isStaticIndoorNavigableFloorId
|
||||
} from '@/data/adapters/navAssetsAdapter'
|
||||
import {
|
||||
defaultStaticNavAssetsProvider,
|
||||
@@ -97,6 +101,7 @@ export class StaticGuideModelRepository implements GuideModelRepository {
|
||||
])
|
||||
|
||||
const floors: GuideModelFloorAsset[] = [...floorIndex.floors]
|
||||
.filter((floor) => isStaticIndoorNavigableFloorId(floor.floorId))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((floor) => ({
|
||||
floorId: floor.floorId,
|
||||
@@ -115,7 +120,7 @@ export class StaticGuideModelRepository implements GuideModelRepository {
|
||||
async loadFloorPois(floorId: string) {
|
||||
const floorIndex = await this.provider.loadFloorIndex()
|
||||
const floor = floorIndex.floors.find((item) => item.floorId === floorId)
|
||||
if (!floor) return []
|
||||
if (!floor || !isStaticIndoorNavigableFloorId(floor.floorId)) return []
|
||||
|
||||
const pois = await this.provider.loadFloorPois(floor.poiDataAsset)
|
||||
return pois
|
||||
@@ -131,7 +136,8 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
||||
|
||||
async loadPackage(): Promise<GuideModelRenderPackage> {
|
||||
const manifest = await this.provider.getManifest()
|
||||
const floors = sortSgsFloors(manifest.floors)
|
||||
const allFloors = sortSgsFloors(manifest.floors)
|
||||
const floors = allFloors.filter(isIndoorNavigableFloor)
|
||||
this.floorsCache = floors
|
||||
|
||||
const bundles = await Promise.all(
|
||||
@@ -153,8 +159,13 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
||||
})
|
||||
.filter((asset) => asset.modelUrl)
|
||||
|
||||
const overviewFloorIndex = floors.findIndex((floor) => floor.floorCode === 'EXTERIOR')
|
||||
const overviewModelUrl = floorAssets[overviewFloorIndex]?.modelUrl || floorAssets[0]?.modelUrl || ''
|
||||
const overviewFloor = allFloors.find((floor) => !isIndoorNavigableFloor(floor))
|
||||
const overviewBundle = overviewFloor
|
||||
? await this.provider.getFloorBundle(String(overviewFloor.floorId)).catch(() => null)
|
||||
: null
|
||||
const overviewModelUrl = resolveSgsAssetUrl(
|
||||
overviewBundle?.model?.modelUrl || overviewBundle?.model?.fallbackModelUrl
|
||||
) || floorAssets[0]?.modelUrl || ''
|
||||
|
||||
return {
|
||||
overviewModelUrl,
|
||||
@@ -164,7 +175,7 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
||||
|
||||
async loadFloorPois(floorId: string) {
|
||||
const manifest = await this.provider.getManifest()
|
||||
this.floorsCache = sortSgsFloors(manifest.floors)
|
||||
this.floorsCache = sortSgsFloors(manifest.floors).filter(isIndoorNavigableFloor)
|
||||
const matchedFloor = this.floorsCache.find((floor) => (
|
||||
String(floor.floorId) === floorId
|
||||
|| floor.floorCode === floorId
|
||||
|
||||
@@ -10,8 +10,13 @@ import type {
|
||||
import {
|
||||
NAV_ROUTE_READINESS
|
||||
} from '@/domain/guideReadiness'
|
||||
import {
|
||||
isIndoorNavigableFloor,
|
||||
isPoiOnIndoorNavigableFloor
|
||||
} from '@/domain/guideFloor'
|
||||
import {
|
||||
formatNavFloorLabel,
|
||||
isStaticIndoorNavigableFloorId,
|
||||
navFloorIdFromLabel,
|
||||
toMuseumPoi
|
||||
} from '@/data/adapters/navAssetsAdapter'
|
||||
@@ -68,6 +73,11 @@ const toLocationPreview = (poi: MuseumPoi): GuideLocationPreview => ({
|
||||
sourceObjectName: poi.sourceObjectName
|
||||
})
|
||||
|
||||
const isSearchableIndoorPoi = (poi: MuseumPoi) => (
|
||||
searchableCategories.has(poi.primaryCategory.id)
|
||||
&& isPoiOnIndoorNavigableFloor(poi)
|
||||
)
|
||||
|
||||
export interface GuideRepository {
|
||||
getAssetBaseUrl(): string
|
||||
getFloors(): Promise<MuseumFloor[]>
|
||||
@@ -94,6 +104,7 @@ export class StaticGuideRepository implements GuideRepository {
|
||||
async getFloors() {
|
||||
const floorIndex = await this.provider.loadFloorIndex()
|
||||
return [...floorIndex.floors]
|
||||
.filter((floor) => isStaticIndoorNavigableFloorId(floor.floorId))
|
||||
.sort((a, b) => b.order - a.order)
|
||||
.map((floor) => ({
|
||||
id: floor.floorId,
|
||||
@@ -111,8 +122,8 @@ export class StaticGuideRepository implements GuideRepository {
|
||||
|
||||
const pois = await this.provider.loadPoiIndex()
|
||||
this.poiCache = pois
|
||||
.filter((poi) => searchableCategories.has(poi.primaryCategory))
|
||||
.map(toMuseumPoi)
|
||||
.filter(isSearchableIndoorPoi)
|
||||
|
||||
return this.poiCache
|
||||
}
|
||||
@@ -208,8 +219,10 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
||||
if (this.floorsCache) return this.floorsCache
|
||||
|
||||
const manifest = await this.provider.getManifest()
|
||||
this.floorAliases = buildSgsFloorAliases(manifest.floors)
|
||||
const indoorFloors = manifest.floors.filter(isIndoorNavigableFloor)
|
||||
this.floorAliases = buildSgsFloorAliases(indoorFloors)
|
||||
this.floorsCache = [...manifest.floors]
|
||||
.filter(isIndoorNavigableFloor)
|
||||
.sort((a, b) => Number(a.sortOrder || 0) - Number(b.sortOrder || 0))
|
||||
.map(toMuseumFloorFromSgs)
|
||||
|
||||
@@ -228,15 +241,18 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
||||
if (this.poiCache) return this.poiCache
|
||||
|
||||
const manifest = await this.provider.getManifest()
|
||||
this.floorAliases = buildSgsFloorAliases(manifest.floors)
|
||||
const indoorFloors = manifest.floors.filter(isIndoorNavigableFloor)
|
||||
const indoorFloorIds = new Set(indoorFloors.map((floor) => String(floor.floorId)))
|
||||
this.floorAliases = buildSgsFloorAliases(indoorFloors)
|
||||
const poiGroups = await Promise.all(
|
||||
manifest.floors.map((floor) => this.provider.getFloorPois(String(floor.floorId)))
|
||||
indoorFloors.map((floor) => this.provider.getFloorPois(String(floor.floorId)))
|
||||
)
|
||||
|
||||
this.poiCache = poiGroups
|
||||
.flat()
|
||||
.map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
|
||||
.filter((poi) => poi.id && poi.name)
|
||||
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
|
||||
|
||||
return this.poiCache
|
||||
}
|
||||
@@ -271,11 +287,11 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
||||
const normalizedFloorId = this.normalizeFloorId(floorId)
|
||||
try {
|
||||
const diagnostics = await this.provider.getFloorDiagnostics(normalizedFloorId)
|
||||
return toGuideFloorDetailFromSgs(diagnostics)
|
||||
return isIndoorNavigableFloor(diagnostics) ? toGuideFloorDetailFromSgs(diagnostics) : null
|
||||
} catch {
|
||||
const manifest = await this.provider.getManifest()
|
||||
const floor = manifest.floors.find((item) => String(item.floorId) === normalizedFloorId)
|
||||
return floor ? toGuideFloorDetailFromSgs(floor) : null
|
||||
return floor && isIndoorNavigableFloor(floor) ? toGuideFloorDetailFromSgs(floor) : null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import type {
|
||||
GuideRouteResult,
|
||||
GuideRouteTarget
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
isIndoorNavigableFloor
|
||||
} from '@/domain/guideFloor'
|
||||
import {
|
||||
applyRouteReadinessGate,
|
||||
NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
@@ -164,7 +167,10 @@ export class StaticGuideRouteRepository implements GuideRouteRepository {
|
||||
|
||||
async listRouteTargets() {
|
||||
const dataset = await this.loadDataset()
|
||||
return dataset.targets
|
||||
return dataset.targets.filter((target) => isIndoorNavigableFloor({
|
||||
floorId: target.floorId,
|
||||
label: target.floorLabel
|
||||
}))
|
||||
}
|
||||
|
||||
async findRoute(startPoiId: string, endPoiId: string) {
|
||||
|
||||
@@ -6,6 +6,9 @@ import type {
|
||||
import {
|
||||
NAV_ROUTE_READINESS
|
||||
} from '@/domain/guideReadiness'
|
||||
import {
|
||||
isIndoorNavigableFloor
|
||||
} from '@/domain/guideFloor'
|
||||
import {
|
||||
GuideRouteError
|
||||
} from '@/repositories/GuideRouteRepository'
|
||||
@@ -63,7 +66,7 @@ export class SdkGuideRouteRepository {
|
||||
async getRouteReadiness(): Promise<GuideRouteReadiness> {
|
||||
try {
|
||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||
const floors = diagnostics.floors || []
|
||||
const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor)
|
||||
|
||||
if (!floors.length) {
|
||||
return createUnavailableReadiness('SDK 地图楼层数据为空')
|
||||
@@ -115,7 +118,7 @@ export class SdkGuideRouteRepository {
|
||||
if (this.targetsCache) return this.targetsCache
|
||||
|
||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||
const floors = diagnostics.floors || []
|
||||
const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor)
|
||||
|
||||
const targets: GuideRouteTarget[] = []
|
||||
|
||||
@@ -196,7 +199,7 @@ export class SdkGuideRouteRepository {
|
||||
// 获取原始 floorId 用于 API 调用
|
||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||
const floorIdMap = new Map<string, string>()
|
||||
for (const floor of diagnostics.floors || []) {
|
||||
for (const floor of (diagnostics.floors || []).filter(isIndoorNavigableFloor)) {
|
||||
floorIdMap.set(toAppFloorId(floor.floorId), stringifyId(floor.floorId))
|
||||
}
|
||||
const rawStartFloorId = floorIdMap.get(startTarget.floorId) || startTarget.floorId
|
||||
|
||||
@@ -3,6 +3,9 @@ import type {
|
||||
GuideRouteResult,
|
||||
GuideRouteTarget
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
isIndoorNavigableFloor
|
||||
} from '@/domain/guideFloor'
|
||||
import {
|
||||
GuideRouteError,
|
||||
type GuideRouteRepository
|
||||
@@ -47,7 +50,11 @@ export class GuideRouteUseCase {
|
||||
async listTargets() {
|
||||
if (this.targetsCache) return this.targetsCache
|
||||
|
||||
this.targetsCache = await this.repository.listRouteTargets()
|
||||
this.targetsCache = (await this.repository.listRouteTargets())
|
||||
.filter((target) => isIndoorNavigableFloor({
|
||||
floorId: target.floorId,
|
||||
label: target.floorLabel
|
||||
}))
|
||||
return this.targetsCache
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user