优化三维导览模型加载与交互体验
This commit is contained in:
@@ -124,6 +124,19 @@ interface MultiFloorModelItem {
|
|||||||
size: THREE.Vector3
|
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
|
type ContainerRef = HTMLElement | { $el?: HTMLElement } | null
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
@@ -154,8 +167,8 @@ const props = withDefaults(defineProps<{
|
|||||||
routePreview: null,
|
routePreview: null,
|
||||||
showRoute: false,
|
showRoute: false,
|
||||||
autoSwitch: true,
|
autoSwitch: true,
|
||||||
autoSwitchThresholdLow: 1.0,
|
autoSwitchThresholdLow: 1.45,
|
||||||
autoSwitchThresholdHigh: 1.3,
|
autoSwitchThresholdHigh: 3.8,
|
||||||
autoSwitchCooldown: 2000
|
autoSwitchCooldown: 2000
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -199,6 +212,9 @@ let dracoLoader: DRACOLoader | null = null
|
|||||||
let activeModel: THREE.Object3D | null = null
|
let activeModel: THREE.Object3D | null = null
|
||||||
let poiGroup: THREE.Group | null = null
|
let poiGroup: THREE.Group | null = null
|
||||||
let routeGroup: 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 activeFocusLabelSprite: THREE.Sprite | null = null
|
||||||
let activeFocusPulseSprite: THREE.Sprite | null = null
|
let activeFocusPulseSprite: THREE.Sprite | null = null
|
||||||
let activeFocusBaseSprite: THREE.Sprite | null = null
|
let activeFocusBaseSprite: THREE.Sprite | null = null
|
||||||
@@ -218,11 +234,20 @@ let autoSwitchOverviewMaxDim = 0
|
|||||||
// 自动切换状态
|
// 自动切换状态
|
||||||
let isAutoSwitchLocked = false
|
let isAutoSwitchLocked = false
|
||||||
let lastAutoSwitchTime = 0
|
let lastAutoSwitchTime = 0
|
||||||
|
let lastControlDistance = 0
|
||||||
|
let floorAutoSwitchProtectedUntil = 0
|
||||||
|
let floorExitThresholdExceededSince = 0
|
||||||
|
let floorExitZoomOutAccumulatedDistance = 0
|
||||||
let autoSwitchTemporarilyDisabled = false
|
let autoSwitchTemporarilyDisabled = false
|
||||||
let autoSwitchDisableTimer: ReturnType<typeof setTimeout> | null = null
|
let autoSwitchDisableTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let isProgrammaticCameraChange = false
|
let isProgrammaticCameraChange = false
|
||||||
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
|
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const floorAutoSwitchProtectionMs = 1600
|
||||||
|
const floorExitZoomOutIntentMinDeltaRatio = 0.01
|
||||||
|
const floorExitZoomOutAccumulatedRatio = 0.12
|
||||||
|
const floorExitThresholdHoldMs = 650
|
||||||
|
|
||||||
const syncControlInteractionOptions = () => {
|
const syncControlInteractionOptions = () => {
|
||||||
if (!controls) return
|
if (!controls) return
|
||||||
|
|
||||||
@@ -549,8 +574,7 @@ const updatePoiVisibilityByDistance = () => {
|
|||||||
const acceptedPositions: THREE.Vector2[] = []
|
const acceptedPositions: THREE.Vector2[] = []
|
||||||
let visibleCount = 0
|
let visibleCount = 0
|
||||||
|
|
||||||
const candidates = poiGroup.children
|
const candidates = getPoiSprites()
|
||||||
.filter((child): child is THREE.Sprite => child instanceof THREE.Sprite && !isPoiAuxiliarySprite(child))
|
|
||||||
.map((sprite) => {
|
.map((sprite) => {
|
||||||
const poi = sprite.userData.poi as RenderPoi | undefined
|
const poi = sprite.userData.poi as RenderPoi | undefined
|
||||||
const screenPosition = sprite.position.clone().project(camera!)
|
const screenPosition = sprite.position.clone().project(camera!)
|
||||||
@@ -744,27 +768,195 @@ const handleResize = () => {
|
|||||||
renderer.setSize(width, height)
|
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) => {
|
object.traverse((child) => {
|
||||||
if (child instanceof THREE.Mesh) {
|
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]
|
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) {
|
if (child instanceof THREE.Sprite) {
|
||||||
child.material.map?.dispose()
|
if (!protectedResources.materials.has(child.material) && !disposedMaterials.has(child.material)) {
|
||||||
child.material.dispose()
|
disposeMaterialTextures(child.material, protectedResources, disposedTextures)
|
||||||
|
child.material.dispose()
|
||||||
|
disposedMaterials.add(child.material)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child instanceof THREE.Line) {
|
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]
|
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 getMaterialColor = (material: THREE.Material) => {
|
||||||
const candidate = material as THREE.Material & { color?: unknown }
|
const candidate = material as THREE.Material & { color?: unknown }
|
||||||
return candidate.color instanceof THREE.Color ? candidate.color : undefined
|
return candidate.color instanceof THREE.Color ? candidate.color : undefined
|
||||||
@@ -943,6 +1135,7 @@ const disposeCachedOverviewModel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clearRoutePreview = () => {
|
const clearRoutePreview = () => {
|
||||||
|
activeRoutePreviewSignature = ''
|
||||||
if (!routeGroup) return
|
if (!routeGroup) return
|
||||||
|
|
||||||
while (routeGroup.children.length) {
|
while (routeGroup.children.length) {
|
||||||
@@ -1073,15 +1266,54 @@ const renderRouteConnectorMarkers = (route: GuideRouteResult, markerSize: number
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderRoutePreview = () => {
|
const routePositionSignature = (position: [number, number, number]) => (
|
||||||
clearRoutePreview()
|
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 route = props.routePreview
|
||||||
const targetRouteGroup = routeGroup
|
const targetRouteGroup = routeGroup
|
||||||
const visibleSegments = getVisibleRouteSegments(route)
|
const visibleSegments = getVisibleRouteSegments(route)
|
||||||
const markerSize = Math.max(getPoiMarkerSize() * 0.82, 2.8)
|
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) => {
|
visibleSegments.forEach((segment) => {
|
||||||
if (segment.points.length < 2) return
|
if (segment.points.length < 2) return
|
||||||
@@ -1104,7 +1336,7 @@ const clearSceneData = () => {
|
|||||||
if (activeModel === cachedOverviewModel) {
|
if (activeModel === cachedOverviewModel) {
|
||||||
cachedOverviewModel.visible = false
|
cachedOverviewModel.visible = false
|
||||||
} else {
|
} else {
|
||||||
disposeObject(activeModel)
|
disposeObject(activeModel, collectReusableModelResources(cachedOverviewModel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1114,14 +1346,7 @@ const clearSceneData = () => {
|
|||||||
disposeFocusBase()
|
disposeFocusBase()
|
||||||
selectedPOI.value = null
|
selectedPOI.value = null
|
||||||
clearRoutePreview()
|
clearRoutePreview()
|
||||||
|
clearPoiGroupChildren()
|
||||||
if (poiGroup) {
|
|
||||||
while (poiGroup.children.length) {
|
|
||||||
const child = poiGroup.children[0]
|
|
||||||
poiGroup.remove(child)
|
|
||||||
disposeObject(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadModel = (url: string, label: string, loadToken?: number) => new Promise<GLTF>((resolve, reject) => {
|
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 = () => {
|
const checkAutoSwitch = () => {
|
||||||
// 检查是否启用自动切换
|
// 检查是否启用自动切换
|
||||||
if (!props.autoSwitch || autoSwitchTemporarilyDisabled || isProgrammaticCameraChange || isLoading.value) {
|
if (!props.autoSwitch || autoSwitchTemporarilyDisabled || isProgrammaticCameraChange || isLoading.value) {
|
||||||
@@ -1215,18 +1497,23 @@ const checkAutoSwitch = () => {
|
|||||||
|
|
||||||
// 获取当前距离
|
// 获取当前距离
|
||||||
const distance = controls.getDistance()
|
const distance = controls.getDistance()
|
||||||
|
const previousDistance = lastControlDistance || distance
|
||||||
|
const distanceDelta = distance - previousDistance
|
||||||
|
lastControlDistance = distance
|
||||||
|
|
||||||
// 自动切换使用全馆稳定尺寸,避免单楼层可见性过滤压低 floor -> overview 阈值。
|
// 自动切换使用全馆稳定尺寸,避免单楼层可见性过滤压低 floor -> overview 阈值。
|
||||||
const maxDim = autoSwitchOverviewMaxDim || getObjectMaxDim(activeModel)
|
const maxDim = autoSwitchOverviewMaxDim || getObjectMaxDim(activeModel)
|
||||||
|
|
||||||
const thresholdLow = maxDim * props.autoSwitchThresholdLow
|
const enterFloorAt = maxDim * props.autoSwitchThresholdLow
|
||||||
const thresholdHigh = maxDim * props.autoSwitchThresholdHigh
|
const exitFloorAt = maxDim * props.autoSwitchThresholdHigh
|
||||||
|
|
||||||
// 判断是否需要切换
|
// 判断是否需要切换
|
||||||
if (activeView.value === 'overview' && distance < thresholdLow) {
|
if (activeView.value === 'overview' && distance < enterFloorAt) {
|
||||||
// 从建筑外观自动切换到单楼层
|
// 从建筑外观自动切换到单楼层
|
||||||
isAutoSwitchLocked = true
|
isAutoSwitchLocked = true
|
||||||
lastAutoSwitchTime = now
|
lastAutoSwitchTime = now
|
||||||
|
floorExitThresholdExceededSince = 0
|
||||||
|
floorExitZoomOutAccumulatedDistance = 0
|
||||||
|
|
||||||
void runAutoSwitchLoad(
|
void runAutoSwitchLoad(
|
||||||
{
|
{
|
||||||
@@ -1238,10 +1525,52 @@ const checkAutoSwitch = () => {
|
|||||||
() => loadFloor(currentFloor.value || props.initialFloorId || 'L1'),
|
() => 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
|
isAutoSwitchLocked = true
|
||||||
lastAutoSwitchTime = now
|
lastAutoSwitchTime = now
|
||||||
|
floorExitThresholdExceededSince = 0
|
||||||
|
floorExitZoomOutAccumulatedDistance = 0
|
||||||
|
|
||||||
void runAutoSwitchLoad(
|
void runAutoSwitchLoad(
|
||||||
{
|
{
|
||||||
@@ -1362,10 +1691,13 @@ const zoomCamera = (direction: 'in' | 'out') => {
|
|||||||
offset.setLength(nextDistance)
|
offset.setLength(nextDistance)
|
||||||
camera.position.copy(controls.target).add(offset)
|
camera.position.copy(controls.target).add(offset)
|
||||||
controls.update()
|
controls.update()
|
||||||
|
lastControlDistance = nextDistance
|
||||||
|
|
||||||
if (direction === 'in' && activeView.value === 'overview' && !isLoading.value && !isAutoSwitchLocked) {
|
if (direction === 'in' && activeView.value === 'overview' && !isLoading.value && !isAutoSwitchLocked) {
|
||||||
isAutoSwitchLocked = true
|
isAutoSwitchLocked = true
|
||||||
lastAutoSwitchTime = Date.now()
|
lastAutoSwitchTime = Date.now()
|
||||||
|
floorExitThresholdExceededSince = 0
|
||||||
|
floorExitZoomOutAccumulatedDistance = 0
|
||||||
|
|
||||||
void runAutoSwitchLoad(
|
void runAutoSwitchLoad(
|
||||||
{
|
{
|
||||||
@@ -1401,6 +1733,7 @@ const loadOverview = async () => {
|
|||||||
cacheAutoSwitchOverviewMaxDim(activeModel)
|
cacheAutoSwitchOverviewMaxDim(activeModel)
|
||||||
targetScene.add(activeModel)
|
targetScene.add(activeModel)
|
||||||
fitCameraToObject(activeModel)
|
fitCameraToObject(activeModel)
|
||||||
|
resetAutoSwitchDistanceTracking()
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
renderRoutePreview()
|
renderRoutePreview()
|
||||||
return
|
return
|
||||||
@@ -1426,6 +1759,7 @@ const loadOverview = async () => {
|
|||||||
cachedSharedModelUrl = packageData.overviewModelUrl
|
cachedSharedModelUrl = packageData.overviewModelUrl
|
||||||
targetScene.add(activeModel)
|
targetScene.add(activeModel)
|
||||||
fitCameraToObject(activeModel)
|
fitCameraToObject(activeModel)
|
||||||
|
resetAutoSwitchDistanceTracking()
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
renderRoutePreview()
|
renderRoutePreview()
|
||||||
}
|
}
|
||||||
@@ -1486,6 +1820,7 @@ const loadFloor = async (floorId: string) => {
|
|||||||
|
|
||||||
if (!activeModel) throw createStaleModelLoadError()
|
if (!activeModel) throw createStaleModelLoadError()
|
||||||
fitCameraToObject(activeModel)
|
fitCameraToObject(activeModel)
|
||||||
|
markFloorAutoSwitchEntry()
|
||||||
|
|
||||||
if (shouldRenderPoiMarkers.value) {
|
if (shouldRenderPoiMarkers.value) {
|
||||||
await loadFloorPOIs(floor, loadToken)
|
await loadFloorPOIs(floor, loadToken)
|
||||||
@@ -1508,9 +1843,81 @@ const loadMultiFloor = async () => {
|
|||||||
group.name = 'GuideMultiFloorModel'
|
group.name = 'GuideMultiFloorModel'
|
||||||
const orderedFloors = [...floorIndex.value].sort((a, b) => a.order - b.order)
|
const orderedFloors = [...floorIndex.value].sort((a, b) => a.order - b.order)
|
||||||
const loadedFloors: MultiFloorModelItem[] = []
|
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) {
|
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 floor = orderedFloors[index]
|
||||||
const label = formatFloorLabel(floor.floorId)
|
const label = formatFloorLabel(floor.floorId)
|
||||||
@@ -1518,6 +1925,7 @@ const loadMultiFloor = async () => {
|
|||||||
if (!isCurrentModelLoad(loadToken)) {
|
if (!isCurrentModelLoad(loadToken)) {
|
||||||
disposeObject(gltf.scene)
|
disposeObject(gltf.scene)
|
||||||
disposeObject(group)
|
disposeObject(group)
|
||||||
|
disposeDetachedMultiFloorModels(loadedFloors, group)
|
||||||
throw createStaleModelLoadError()
|
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)
|
applyMultiFloorLayout(loadedFloors)
|
||||||
loadedFloors.forEach((item) => group.add(item.model))
|
loadedFloors.forEach((item) => group.add(item.model))
|
||||||
|
|
||||||
const targetScene = scene
|
const targetScene = scene
|
||||||
if (!targetScene) {
|
if (!targetScene) {
|
||||||
disposeObject(group)
|
disposeObject(group)
|
||||||
|
disposeDetachedMultiFloorModels(loadedFloors, group)
|
||||||
throw createStaleModelLoadError()
|
throw createStaleModelLoadError()
|
||||||
}
|
}
|
||||||
|
|
||||||
activeModel = group
|
activeModel = group
|
||||||
targetScene.add(activeModel)
|
targetScene.add(activeModel)
|
||||||
fitCameraToObject(activeModel)
|
fitCameraToObject(activeModel)
|
||||||
|
resetAutoSwitchDistanceTracking()
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
renderRoutePreview()
|
renderRoutePreview()
|
||||||
}
|
}
|
||||||
@@ -1873,25 +2289,16 @@ const setPoiSpriteFocusStyle = (sprite: THREE.Sprite, focused: boolean) => {
|
|||||||
const updatePoiMarkerFocus = () => {
|
const updatePoiMarkerFocus = () => {
|
||||||
if (!poiGroup) return
|
if (!poiGroup) return
|
||||||
|
|
||||||
poiGroup.children.forEach((child) => {
|
getPoiSprites().forEach((sprite) => {
|
||||||
if (
|
const poi = sprite.userData.poi as RenderPoi | undefined
|
||||||
child instanceof THREE.Sprite
|
setPoiSpriteFocusStyle(sprite, Boolean(poi && poi.id === activeFocusPoiId.value))
|
||||||
&& !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))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
refreshPoiVisibilityByDistance()
|
refreshPoiVisibilityByDistance()
|
||||||
}
|
}
|
||||||
|
|
||||||
const findPoiSprite = (poiId: string) => (
|
const findPoiSprite = (poiId: string) => (
|
||||||
poiGroup?.children.find((child): child is THREE.Sprite => (
|
getPoiSprites().find((sprite) => (sprite.userData.poi as RenderPoi | undefined)?.id === poiId)
|
||||||
child instanceof THREE.Sprite && (child.userData.poi as RenderPoi | undefined)?.id === poiId
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const findLoadedPoi = (poiId: string) => (
|
const findLoadedPoi = (poiId: string) => (
|
||||||
@@ -1932,19 +2339,12 @@ const addFocusMarkerFromRequest = (request: TargetPoiFocusRequest) => {
|
|||||||
return poi
|
return poi
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
|
const createPoiMarkerGroup = (floorId: string, pois: RenderPoi[], markerSize: number) => {
|
||||||
if (!poiGroup) return
|
const group = new THREE.Group()
|
||||||
|
group.name = `GuideModelPOI_${floorId}`
|
||||||
|
group.userData.floorId = floorId
|
||||||
|
|
||||||
const validPois = (await props.modelSource.loadFloorPois(floor.floorId))
|
pois.forEach((poi) => {
|
||||||
.filter((poi) => shouldShowPoiInCurrentMode(poi))
|
|
||||||
if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) return
|
|
||||||
|
|
||||||
const targetPoiGroup = poiGroup
|
|
||||||
if (!targetPoiGroup) return
|
|
||||||
|
|
||||||
const markerSize = getPoiMarkerSize()
|
|
||||||
|
|
||||||
validPois.forEach((poi) => {
|
|
||||||
const [x, y, z] = poi.positionGltf!
|
const [x, y, z] = poi.positionGltf!
|
||||||
const sprite = new THREE.Sprite(createPoiMaterial(poi))
|
const sprite = new THREE.Sprite(createPoiMaterial(poi))
|
||||||
sprite.position.set(x, y + markerSize * 0.5, z)
|
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)
|
sprite.userData.isCorePoi = corePoiCategories.has(poi.primaryCategory)
|
||||||
setPoiSpriteFocusStyle(sprite, poi.id === activeFocusPoiId.value)
|
setPoiSpriteFocusStyle(sprite, poi.id === activeFocusPoiId.value)
|
||||||
sprite.visible = shouldShowPoiAtDistance(poi, getPoiVisibilityTier())
|
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) => {
|
const clearMapSelection = (shouldEmit = true) => {
|
||||||
@@ -1997,7 +2444,7 @@ const handleSceneTap = (event: PointerEvent) => {
|
|||||||
raycaster.setFromCamera(pointer, camera)
|
raycaster.setFromCamera(pointer, camera)
|
||||||
|
|
||||||
if (activeView.value === 'floor' && poiGroup) {
|
if (activeView.value === 'floor' && poiGroup) {
|
||||||
const poiHits = raycaster.intersectObjects(poiGroup.children, false)
|
const poiHits = raycaster.intersectObjects(getPoiSprites(), false)
|
||||||
const hit = poiHits.find((item) => (
|
const hit = poiHits.find((item) => (
|
||||||
item.object.visible
|
item.object.visible
|
||||||
&& item.object instanceof THREE.Sprite
|
&& item.object instanceof THREE.Sprite
|
||||||
@@ -2126,7 +2573,7 @@ const focusTargetPoi = async (request: TargetPoiFocusRequest) => {
|
|||||||
await loadFloor(request.floorId)
|
await loadFloor(request.floorId)
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
emit('floorChange', request.floorId)
|
emit('floorChange', request.floorId)
|
||||||
} else if (shouldRenderPoiMarkers.value && poiGroup && poiGroup.children.length === 0) {
|
} else if (shouldRenderPoiMarkers.value && !getPoiSprites().length) {
|
||||||
await loadFloorPOIs(floor, modelLoadVersion)
|
await loadFloorPOIs(floor, modelLoadVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2311,8 +2758,13 @@ const disposeScene = () => {
|
|||||||
programmaticCameraTimer = null
|
programmaticCameraTimer = null
|
||||||
}
|
}
|
||||||
isProgrammaticCameraChange = false
|
isProgrammaticCameraChange = false
|
||||||
|
lastControlDistance = 0
|
||||||
|
floorAutoSwitchProtectedUntil = 0
|
||||||
|
floorExitThresholdExceededSince = 0
|
||||||
|
floorExitZoomOutAccumulatedDistance = 0
|
||||||
|
|
||||||
clearSceneData()
|
clearSceneData()
|
||||||
|
disposePoiMarkerCache()
|
||||||
disposeCachedOverviewModel()
|
disposeCachedOverviewModel()
|
||||||
scene?.clear()
|
scene?.clear()
|
||||||
scene = null
|
scene = null
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
GuideRouteResult
|
GuideRouteResult
|
||||||
} from '@/domain/museum'
|
} from '@/domain/museum'
|
||||||
|
import {
|
||||||
|
isIndoorNavigableFloor
|
||||||
|
} from '@/domain/guideFloor'
|
||||||
|
|
||||||
interface GuideFloorOption {
|
interface GuideFloorOption {
|
||||||
id: string
|
id: string
|
||||||
@@ -367,7 +370,8 @@ const indoorRendererRef = ref<{
|
|||||||
disableAutoSwitchTemporarily?: (durationMs: number) => void
|
disableAutoSwitchTemporarily?: (durationMs: number) => void
|
||||||
} | null>(null)
|
} | 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
|
// #ifdef H5
|
||||||
const effectiveIndoorModelSource = computed(() => props.indoorModelSource)
|
const effectiveIndoorModelSource = computed(() => props.indoorModelSource)
|
||||||
// #endif
|
// #endif
|
||||||
@@ -376,13 +380,13 @@ const showIndoorRightControls = computed(() => (
|
|||||||
))
|
))
|
||||||
|
|
||||||
const activeFloorId = computed(() => {
|
const activeFloorId = computed(() => {
|
||||||
const matchedFloor = props.floors.find((floor) => (
|
const matchedFloor = indoorFloors.value.find((floor) => (
|
||||||
floor.id === props.activeFloor || floor.label === props.activeFloor
|
floor.id === props.activeFloor || floor.label === props.activeFloor
|
||||||
))
|
))
|
||||||
if (matchedFloor) return matchedFloor.id
|
if (matchedFloor) return matchedFloor.id
|
||||||
|
|
||||||
const normalizedFloorId = props.normalizeFloorId(props.activeFloor)
|
const normalizedFloorId = props.normalizeFloorId(props.activeFloor)
|
||||||
const normalizedFloor = props.floors.find((floor) => (
|
const normalizedFloor = indoorFloors.value.find((floor) => (
|
||||||
floor.id === normalizedFloorId || floor.label === normalizedFloorId
|
floor.id === normalizedFloorId || floor.label === normalizedFloorId
|
||||||
))
|
))
|
||||||
return normalizedFloor?.id || normalizedFloorId
|
return normalizedFloor?.id || normalizedFloorId
|
||||||
@@ -497,7 +501,7 @@ const handleMoreTap = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleThreeFloorChange = (floorId: string) => {
|
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('floorChange', floor?.label || floorId)
|
||||||
emit('indoorViewChange', 'floor')
|
emit('indoorViewChange', 'floor')
|
||||||
emit('layerModeChange', 'single')
|
emit('layerModeChange', 'single')
|
||||||
|
|||||||
@@ -65,6 +65,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import {
|
||||||
|
isIndoorNavigableFloor
|
||||||
|
} from '@/domain/guideFloor'
|
||||||
|
|
||||||
export interface RoutePointOption {
|
export interface RoutePointOption {
|
||||||
poiId: string
|
poiId: string
|
||||||
@@ -128,9 +131,14 @@ watch(
|
|||||||
const normalizedKeyword = computed(() => keyword.value.trim().toLowerCase())
|
const normalizedKeyword = computed(() => keyword.value.trim().toLowerCase())
|
||||||
|
|
||||||
const filteredOptions = computed(() => {
|
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 = [
|
const fields = [
|
||||||
option.name,
|
option.name,
|
||||||
option.floorId,
|
option.floorId,
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import type {
|
|||||||
StaticNavPoiCategoryPayload,
|
StaticNavPoiCategoryPayload,
|
||||||
StaticNavPoiPayload
|
StaticNavPoiPayload
|
||||||
} from '@/data/providers/staticNavAssetsProvider'
|
} from '@/data/providers/staticNavAssetsProvider'
|
||||||
|
import {
|
||||||
|
isIndoorNavigableFloor
|
||||||
|
} from '@/domain/guideFloor'
|
||||||
|
|
||||||
const floorIdPattern = /^L(-?\d+(?:\.\d+)?)$/
|
const floorIdPattern = /^L(-?\d+(?:\.\d+)?)$/
|
||||||
|
|
||||||
@@ -31,6 +34,13 @@ export const navFloorIdFromLabel = (labelOrId: string) => {
|
|||||||
return labelOrId
|
return labelOrId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isStaticIndoorNavigableFloorId = (floorId: string) => (
|
||||||
|
isIndoorNavigableFloor({
|
||||||
|
floorId,
|
||||||
|
label: formatNavFloorLabel(floorId)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const toCategory = (category: StaticNavPoiCategoryPayload): MuseumCategory => ({
|
const toCategory = (category: StaticNavPoiCategoryPayload): MuseumCategory => ({
|
||||||
id: category.topCategory,
|
id: category.topCategory,
|
||||||
label: category.topCategoryZh,
|
label: category.topCategoryZh,
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import type {
|
|||||||
import {
|
import {
|
||||||
NAV_ROUTE_UNAVAILABLE_MESSAGE
|
NAV_ROUTE_UNAVAILABLE_MESSAGE
|
||||||
} from '@/domain/guideReadiness'
|
} from '@/domain/guideReadiness'
|
||||||
|
import {
|
||||||
|
isIndoorNavigableFloor
|
||||||
|
} from '@/domain/guideFloor'
|
||||||
import type {
|
import type {
|
||||||
SgsFloorDiagnosticsPayload,
|
SgsFloorDiagnosticsPayload,
|
||||||
SgsMapDiagnosticsPayload,
|
SgsMapDiagnosticsPayload,
|
||||||
@@ -129,6 +132,8 @@ export const buildSgsFloorAliases = (floors: SgsSdkFloorSummaryPayload[]) => {
|
|||||||
const aliases = new Map<string, string>()
|
const aliases = new Map<string, string>()
|
||||||
|
|
||||||
floors.forEach((floor) => {
|
floors.forEach((floor) => {
|
||||||
|
if (!isIndoorNavigableFloor(floor)) return
|
||||||
|
|
||||||
const floorId = stringifyId(floor.floorId)
|
const floorId = stringifyId(floor.floorId)
|
||||||
if (!floorId) return
|
if (!floorId) return
|
||||||
|
|
||||||
@@ -242,16 +247,17 @@ export const toGuideMapDiagnostics = (
|
|||||||
const floors = diagnostics.floors?.length
|
const floors = diagnostics.floors?.length
|
||||||
? diagnostics.floors.map(toGuideFloorDetailFromSgs)
|
? diagnostics.floors.map(toGuideFloorDetailFromSgs)
|
||||||
: (manifest?.floors || []).map(toGuideFloorDetailFromSgs)
|
: (manifest?.floors || []).map(toGuideFloorDetailFromSgs)
|
||||||
|
const indoorFloors = floors.filter(isIndoorNavigableFloor)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mapId: stringifyId(diagnostics.mapId || manifest?.mapId),
|
mapId: stringifyId(diagnostics.mapId || manifest?.mapId),
|
||||||
mapName: diagnostics.mapName || manifest?.mapName || 'SGS 地图',
|
mapName: diagnostics.mapName || manifest?.mapName || 'SGS 地图',
|
||||||
sdkVersion: diagnostics.sdkVersion || manifest?.sdkVersion || undefined,
|
sdkVersion: diagnostics.sdkVersion || manifest?.sdkVersion || undefined,
|
||||||
status: normalizeStatus(diagnostics.status),
|
status: normalizeStatus(diagnostics.status),
|
||||||
floors,
|
floors: indoorFloors,
|
||||||
summary: {
|
summary: {
|
||||||
floorCount: toNumber(summary.floorCount, floors.length),
|
floorCount: indoorFloors.length,
|
||||||
modelReadyFloorCount: toNumber(summary.modelReadyFloorCount),
|
modelReadyFloorCount: indoorFloors.filter((floor) => floor.modelReady === true).length || toNumber(summary.modelReadyFloorCount),
|
||||||
poiCount: toNumber(summary.poiCount),
|
poiCount: toNumber(summary.poiCount),
|
||||||
spaceCount: toNumber(summary.spaceCount),
|
spaceCount: toNumber(summary.spaceCount),
|
||||||
guideStopCount: toNumber(summary.guideStopCount),
|
guideStopCount: toNumber(summary.guideStopCount),
|
||||||
@@ -267,7 +273,7 @@ export const toRouteReadinessFromSgsDiagnostics = (
|
|||||||
diagnostics: SgsMapDiagnosticsPayload
|
diagnostics: SgsMapDiagnosticsPayload
|
||||||
): GuideRouteReadiness => {
|
): GuideRouteReadiness => {
|
||||||
const warnings = diagnostics.warnings || []
|
const warnings = diagnostics.warnings || []
|
||||||
const floors = diagnostics.floors || []
|
const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor)
|
||||||
const notReadyFloors = floors.filter((floor) => (
|
const notReadyFloors = floors.filter((floor) => (
|
||||||
floor.modelReady !== true
|
floor.modelReady !== true
|
||||||
|| floor.routePlanningReady !== true
|
|| floor.routePlanningReady !== true
|
||||||
|
|||||||
@@ -126,7 +126,7 @@
|
|||||||
<text class="more-action-desc">{{ routePlannerEntryDesc }}</text>
|
<text class="more-action-desc">{{ routePlannerEntryDesc }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="more-action" @tap="handleMoreOutdoorNav">
|
<view class="more-action" @tap="handleMoreOutdoorNav">
|
||||||
<text class="more-action-title">室外导航</text>
|
<text class="more-action-title">来馆</text>
|
||||||
<text class="more-action-desc">导航到博物馆</text>
|
<text class="more-action-desc">导航到博物馆</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -591,9 +591,8 @@ const buildExplainHallItems = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleGuideSearchTap = () => {
|
const handleGuideSearchTap = () => {
|
||||||
uni.showToast({
|
uni.navigateTo({
|
||||||
title: '功能待开放',
|
url: '/pages/search/index'
|
||||||
icon: 'none'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,9 @@ import type {
|
|||||||
import {
|
import {
|
||||||
dataSourceConfig
|
dataSourceConfig
|
||||||
} from '@/config/dataSource'
|
} from '@/config/dataSource'
|
||||||
|
import {
|
||||||
|
isIndoorNavigableFloor
|
||||||
|
} from '@/domain/guideFloor'
|
||||||
import {
|
import {
|
||||||
formatSgsFloorLabel,
|
formatSgsFloorLabel,
|
||||||
toMuseumPoiFromSgs
|
toMuseumPoiFromSgs
|
||||||
@@ -17,7 +20,8 @@ import {
|
|||||||
type SgsSdkFloorSummaryPayload
|
type SgsSdkFloorSummaryPayload
|
||||||
} from '@/data/providers/sgsSdkApiProvider'
|
} from '@/data/providers/sgsSdkApiProvider'
|
||||||
import {
|
import {
|
||||||
formatNavFloorLabel
|
formatNavFloorLabel,
|
||||||
|
isStaticIndoorNavigableFloorId
|
||||||
} from '@/data/adapters/navAssetsAdapter'
|
} from '@/data/adapters/navAssetsAdapter'
|
||||||
import {
|
import {
|
||||||
defaultStaticNavAssetsProvider,
|
defaultStaticNavAssetsProvider,
|
||||||
@@ -97,6 +101,7 @@ export class StaticGuideModelRepository implements GuideModelRepository {
|
|||||||
])
|
])
|
||||||
|
|
||||||
const floors: GuideModelFloorAsset[] = [...floorIndex.floors]
|
const floors: GuideModelFloorAsset[] = [...floorIndex.floors]
|
||||||
|
.filter((floor) => isStaticIndoorNavigableFloorId(floor.floorId))
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
.map((floor) => ({
|
.map((floor) => ({
|
||||||
floorId: floor.floorId,
|
floorId: floor.floorId,
|
||||||
@@ -115,7 +120,7 @@ export class StaticGuideModelRepository implements GuideModelRepository {
|
|||||||
async loadFloorPois(floorId: string) {
|
async loadFloorPois(floorId: string) {
|
||||||
const floorIndex = await this.provider.loadFloorIndex()
|
const floorIndex = await this.provider.loadFloorIndex()
|
||||||
const floor = floorIndex.floors.find((item) => item.floorId === floorId)
|
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)
|
const pois = await this.provider.loadFloorPois(floor.poiDataAsset)
|
||||||
return pois
|
return pois
|
||||||
@@ -131,7 +136,8 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
|||||||
|
|
||||||
async loadPackage(): Promise<GuideModelRenderPackage> {
|
async loadPackage(): Promise<GuideModelRenderPackage> {
|
||||||
const manifest = await this.provider.getManifest()
|
const manifest = await this.provider.getManifest()
|
||||||
const floors = sortSgsFloors(manifest.floors)
|
const allFloors = sortSgsFloors(manifest.floors)
|
||||||
|
const floors = allFloors.filter(isIndoorNavigableFloor)
|
||||||
this.floorsCache = floors
|
this.floorsCache = floors
|
||||||
|
|
||||||
const bundles = await Promise.all(
|
const bundles = await Promise.all(
|
||||||
@@ -153,8 +159,13 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
|||||||
})
|
})
|
||||||
.filter((asset) => asset.modelUrl)
|
.filter((asset) => asset.modelUrl)
|
||||||
|
|
||||||
const overviewFloorIndex = floors.findIndex((floor) => floor.floorCode === 'EXTERIOR')
|
const overviewFloor = allFloors.find((floor) => !isIndoorNavigableFloor(floor))
|
||||||
const overviewModelUrl = floorAssets[overviewFloorIndex]?.modelUrl || floorAssets[0]?.modelUrl || ''
|
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 {
|
return {
|
||||||
overviewModelUrl,
|
overviewModelUrl,
|
||||||
@@ -164,7 +175,7 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository {
|
|||||||
|
|
||||||
async loadFloorPois(floorId: string) {
|
async loadFloorPois(floorId: string) {
|
||||||
const manifest = await this.provider.getManifest()
|
const manifest = await this.provider.getManifest()
|
||||||
this.floorsCache = sortSgsFloors(manifest.floors)
|
this.floorsCache = sortSgsFloors(manifest.floors).filter(isIndoorNavigableFloor)
|
||||||
const matchedFloor = this.floorsCache.find((floor) => (
|
const matchedFloor = this.floorsCache.find((floor) => (
|
||||||
String(floor.floorId) === floorId
|
String(floor.floorId) === floorId
|
||||||
|| floor.floorCode === floorId
|
|| floor.floorCode === floorId
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ import type {
|
|||||||
import {
|
import {
|
||||||
NAV_ROUTE_READINESS
|
NAV_ROUTE_READINESS
|
||||||
} from '@/domain/guideReadiness'
|
} from '@/domain/guideReadiness'
|
||||||
|
import {
|
||||||
|
isIndoorNavigableFloor,
|
||||||
|
isPoiOnIndoorNavigableFloor
|
||||||
|
} from '@/domain/guideFloor'
|
||||||
import {
|
import {
|
||||||
formatNavFloorLabel,
|
formatNavFloorLabel,
|
||||||
|
isStaticIndoorNavigableFloorId,
|
||||||
navFloorIdFromLabel,
|
navFloorIdFromLabel,
|
||||||
toMuseumPoi
|
toMuseumPoi
|
||||||
} from '@/data/adapters/navAssetsAdapter'
|
} from '@/data/adapters/navAssetsAdapter'
|
||||||
@@ -68,6 +73,11 @@ const toLocationPreview = (poi: MuseumPoi): GuideLocationPreview => ({
|
|||||||
sourceObjectName: poi.sourceObjectName
|
sourceObjectName: poi.sourceObjectName
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isSearchableIndoorPoi = (poi: MuseumPoi) => (
|
||||||
|
searchableCategories.has(poi.primaryCategory.id)
|
||||||
|
&& isPoiOnIndoorNavigableFloor(poi)
|
||||||
|
)
|
||||||
|
|
||||||
export interface GuideRepository {
|
export interface GuideRepository {
|
||||||
getAssetBaseUrl(): string
|
getAssetBaseUrl(): string
|
||||||
getFloors(): Promise<MuseumFloor[]>
|
getFloors(): Promise<MuseumFloor[]>
|
||||||
@@ -94,6 +104,7 @@ export class StaticGuideRepository implements GuideRepository {
|
|||||||
async getFloors() {
|
async getFloors() {
|
||||||
const floorIndex = await this.provider.loadFloorIndex()
|
const floorIndex = await this.provider.loadFloorIndex()
|
||||||
return [...floorIndex.floors]
|
return [...floorIndex.floors]
|
||||||
|
.filter((floor) => isStaticIndoorNavigableFloorId(floor.floorId))
|
||||||
.sort((a, b) => b.order - a.order)
|
.sort((a, b) => b.order - a.order)
|
||||||
.map((floor) => ({
|
.map((floor) => ({
|
||||||
id: floor.floorId,
|
id: floor.floorId,
|
||||||
@@ -111,8 +122,8 @@ export class StaticGuideRepository implements GuideRepository {
|
|||||||
|
|
||||||
const pois = await this.provider.loadPoiIndex()
|
const pois = await this.provider.loadPoiIndex()
|
||||||
this.poiCache = pois
|
this.poiCache = pois
|
||||||
.filter((poi) => searchableCategories.has(poi.primaryCategory))
|
|
||||||
.map(toMuseumPoi)
|
.map(toMuseumPoi)
|
||||||
|
.filter(isSearchableIndoorPoi)
|
||||||
|
|
||||||
return this.poiCache
|
return this.poiCache
|
||||||
}
|
}
|
||||||
@@ -208,8 +219,10 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
|||||||
if (this.floorsCache) return this.floorsCache
|
if (this.floorsCache) return this.floorsCache
|
||||||
|
|
||||||
const manifest = await this.provider.getManifest()
|
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]
|
this.floorsCache = [...manifest.floors]
|
||||||
|
.filter(isIndoorNavigableFloor)
|
||||||
.sort((a, b) => Number(a.sortOrder || 0) - Number(b.sortOrder || 0))
|
.sort((a, b) => Number(a.sortOrder || 0) - Number(b.sortOrder || 0))
|
||||||
.map(toMuseumFloorFromSgs)
|
.map(toMuseumFloorFromSgs)
|
||||||
|
|
||||||
@@ -228,15 +241,18 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
|||||||
if (this.poiCache) return this.poiCache
|
if (this.poiCache) return this.poiCache
|
||||||
|
|
||||||
const manifest = await this.provider.getManifest()
|
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(
|
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
|
this.poiCache = poiGroups
|
||||||
.flat()
|
.flat()
|
||||||
.map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
|
.map((poi) => toMuseumPoiFromSgs(poi, manifest.floors))
|
||||||
.filter((poi) => poi.id && poi.name)
|
.filter((poi) => poi.id && poi.name)
|
||||||
|
.filter((poi) => indoorFloorIds.has(String(poi.floorId)) || isPoiOnIndoorNavigableFloor(poi))
|
||||||
|
|
||||||
return this.poiCache
|
return this.poiCache
|
||||||
}
|
}
|
||||||
@@ -271,11 +287,11 @@ export class SgsSdkGuideRepository implements GuideRepository {
|
|||||||
const normalizedFloorId = this.normalizeFloorId(floorId)
|
const normalizedFloorId = this.normalizeFloorId(floorId)
|
||||||
try {
|
try {
|
||||||
const diagnostics = await this.provider.getFloorDiagnostics(normalizedFloorId)
|
const diagnostics = await this.provider.getFloorDiagnostics(normalizedFloorId)
|
||||||
return toGuideFloorDetailFromSgs(diagnostics)
|
return isIndoorNavigableFloor(diagnostics) ? toGuideFloorDetailFromSgs(diagnostics) : null
|
||||||
} catch {
|
} catch {
|
||||||
const manifest = await this.provider.getManifest()
|
const manifest = await this.provider.getManifest()
|
||||||
const floor = manifest.floors.find((item) => String(item.floorId) === normalizedFloorId)
|
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,
|
GuideRouteResult,
|
||||||
GuideRouteTarget
|
GuideRouteTarget
|
||||||
} from '@/domain/museum'
|
} from '@/domain/museum'
|
||||||
|
import {
|
||||||
|
isIndoorNavigableFloor
|
||||||
|
} from '@/domain/guideFloor'
|
||||||
import {
|
import {
|
||||||
applyRouteReadinessGate,
|
applyRouteReadinessGate,
|
||||||
NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||||
@@ -164,7 +167,10 @@ export class StaticGuideRouteRepository implements GuideRouteRepository {
|
|||||||
|
|
||||||
async listRouteTargets() {
|
async listRouteTargets() {
|
||||||
const dataset = await this.loadDataset()
|
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) {
|
async findRoute(startPoiId: string, endPoiId: string) {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import type {
|
|||||||
import {
|
import {
|
||||||
NAV_ROUTE_READINESS
|
NAV_ROUTE_READINESS
|
||||||
} from '@/domain/guideReadiness'
|
} from '@/domain/guideReadiness'
|
||||||
|
import {
|
||||||
|
isIndoorNavigableFloor
|
||||||
|
} from '@/domain/guideFloor'
|
||||||
import {
|
import {
|
||||||
GuideRouteError
|
GuideRouteError
|
||||||
} from '@/repositories/GuideRouteRepository'
|
} from '@/repositories/GuideRouteRepository'
|
||||||
@@ -63,7 +66,7 @@ export class SdkGuideRouteRepository {
|
|||||||
async getRouteReadiness(): Promise<GuideRouteReadiness> {
|
async getRouteReadiness(): Promise<GuideRouteReadiness> {
|
||||||
try {
|
try {
|
||||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||||
const floors = diagnostics.floors || []
|
const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor)
|
||||||
|
|
||||||
if (!floors.length) {
|
if (!floors.length) {
|
||||||
return createUnavailableReadiness('SDK 地图楼层数据为空')
|
return createUnavailableReadiness('SDK 地图楼层数据为空')
|
||||||
@@ -115,7 +118,7 @@ export class SdkGuideRouteRepository {
|
|||||||
if (this.targetsCache) return this.targetsCache
|
if (this.targetsCache) return this.targetsCache
|
||||||
|
|
||||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||||
const floors = diagnostics.floors || []
|
const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor)
|
||||||
|
|
||||||
const targets: GuideRouteTarget[] = []
|
const targets: GuideRouteTarget[] = []
|
||||||
|
|
||||||
@@ -196,7 +199,7 @@ export class SdkGuideRouteRepository {
|
|||||||
// 获取原始 floorId 用于 API 调用
|
// 获取原始 floorId 用于 API 调用
|
||||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||||
const floorIdMap = new Map<string, string>()
|
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))
|
floorIdMap.set(toAppFloorId(floor.floorId), stringifyId(floor.floorId))
|
||||||
}
|
}
|
||||||
const rawStartFloorId = floorIdMap.get(startTarget.floorId) || startTarget.floorId
|
const rawStartFloorId = floorIdMap.get(startTarget.floorId) || startTarget.floorId
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import type {
|
|||||||
GuideRouteResult,
|
GuideRouteResult,
|
||||||
GuideRouteTarget
|
GuideRouteTarget
|
||||||
} from '@/domain/museum'
|
} from '@/domain/museum'
|
||||||
|
import {
|
||||||
|
isIndoorNavigableFloor
|
||||||
|
} from '@/domain/guideFloor'
|
||||||
import {
|
import {
|
||||||
GuideRouteError,
|
GuideRouteError,
|
||||||
type GuideRouteRepository
|
type GuideRouteRepository
|
||||||
@@ -47,7 +50,11 @@ export class GuideRouteUseCase {
|
|||||||
async listTargets() {
|
async listTargets() {
|
||||||
if (this.targetsCache) return this.targetsCache
|
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
|
return this.targetsCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user