chore: solidify guide P1 snapshot
This commit is contained in:
@@ -101,6 +101,24 @@ interface CleanPoi {
|
||||
positionGltf?: [number, number, number]
|
||||
}
|
||||
|
||||
interface TargetPoiFocusRequest {
|
||||
requestId: number | string
|
||||
poiId: string
|
||||
name?: string
|
||||
floorId: string
|
||||
floorLabel?: string
|
||||
primaryCategoryZh?: string
|
||||
positionGltf?: [number, number, number]
|
||||
}
|
||||
|
||||
interface TargetPoiFocusResult {
|
||||
requestId: number | string
|
||||
poiId: string
|
||||
floorId: string
|
||||
status: 'focused' | 'missing' | 'error'
|
||||
message?: string
|
||||
}
|
||||
|
||||
interface FloorPoiData {
|
||||
status: string
|
||||
floorId: string
|
||||
@@ -115,17 +133,20 @@ const props = withDefaults(defineProps<{
|
||||
initialView?: ViewMode
|
||||
showControls?: boolean
|
||||
showPoi?: boolean
|
||||
targetFocus?: TargetPoiFocusRequest | null
|
||||
}>(), {
|
||||
assetBaseUrl: NAV_ASSET_BASE_URL,
|
||||
initialFloorId: 'L1',
|
||||
initialView: 'overview',
|
||||
showControls: true,
|
||||
showPoi: false
|
||||
showPoi: false,
|
||||
targetFocus: null
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
floorChange: [floorId: string]
|
||||
poiClick: [poi: CleanPoi]
|
||||
targetFocus: [result: TargetPoiFocusResult]
|
||||
}>()
|
||||
|
||||
const containerRef = ref<ContainerRef>(null)
|
||||
@@ -136,6 +157,7 @@ const loadingMessage = ref('正在读取室内导览资源...')
|
||||
const activeView = ref<ViewMode>(props.initialView)
|
||||
const currentFloor = ref(props.initialFloorId)
|
||||
const selectedPOI = ref<CleanPoi | null>(null)
|
||||
const activeFocusPoiId = ref('')
|
||||
const floorIndex = ref<FloorIndexItem[]>([])
|
||||
|
||||
const floors = computed<FloorOption[]>(() => (
|
||||
@@ -146,6 +168,7 @@ const floors = computed<FloorOption[]>(() => (
|
||||
label: formatFloorLabel(floor.floorId)
|
||||
}))
|
||||
))
|
||||
const shouldRenderPoiMarkers = computed(() => props.showPoi || props.showControls || Boolean(props.targetFocus))
|
||||
|
||||
let manifest: CleanNavManifest | null = null
|
||||
let scene: THREE.Scene | null = null
|
||||
@@ -158,6 +181,8 @@ let poiGroup: THREE.Group | null = null
|
||||
let animationId = 0
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let isDisposed = false
|
||||
let pendingTargetFocus: TargetPoiFocusRequest | null = null
|
||||
let targetFocusQueue: Promise<unknown> = Promise.resolve()
|
||||
|
||||
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
|
||||
|
||||
@@ -410,7 +435,7 @@ const loadFloor = async (floorId: string) => {
|
||||
scene.add(activeModel)
|
||||
fitCameraToObject(activeModel)
|
||||
|
||||
if (props.showPoi || props.showControls) {
|
||||
if (shouldRenderPoiMarkers.value) {
|
||||
await loadFloorPOIs(floor)
|
||||
}
|
||||
}
|
||||
@@ -463,20 +488,90 @@ const getPoiColor = (category: string) => {
|
||||
return colorMap[category] || '#1f2329'
|
||||
}
|
||||
|
||||
const getPoiMarkerSize = () => (
|
||||
activeModel
|
||||
? Math.max(new THREE.Box3().setFromObject(activeModel).getSize(new THREE.Vector3()).length() * 0.012, 2.4)
|
||||
: 3
|
||||
)
|
||||
|
||||
const setPoiSpriteFocusStyle = (sprite: THREE.Sprite, focused: boolean) => {
|
||||
const baseScale = typeof sprite.userData.baseScale === 'number'
|
||||
? sprite.userData.baseScale
|
||||
: sprite.scale.x
|
||||
const scale = focused ? baseScale * 1.55 : baseScale
|
||||
|
||||
sprite.scale.set(scale, scale, scale)
|
||||
sprite.renderOrder = focused ? 20 : 10
|
||||
sprite.material.opacity = focused ? 1 : 0.78
|
||||
}
|
||||
|
||||
const updatePoiMarkerFocus = () => {
|
||||
if (!poiGroup) return
|
||||
|
||||
poiGroup.children.forEach((child) => {
|
||||
if (child instanceof THREE.Sprite) {
|
||||
const poi = child.userData.poi as CleanPoi | undefined
|
||||
setPoiSpriteFocusStyle(child, Boolean(poi && poi.id === activeFocusPoiId.value))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const findPoiSprite = (poiId: string) => (
|
||||
poiGroup?.children.find((child): child is THREE.Sprite => (
|
||||
child instanceof THREE.Sprite && (child.userData.poi as CleanPoi | undefined)?.id === poiId
|
||||
))
|
||||
)
|
||||
|
||||
const findLoadedPoi = (poiId: string) => (
|
||||
findPoiSprite(poiId)?.userData.poi as CleanPoi | undefined
|
||||
)
|
||||
|
||||
const createPoiFromFocusRequest = (request: TargetPoiFocusRequest): CleanPoi | null => {
|
||||
if (!request.positionGltf) return null
|
||||
|
||||
return {
|
||||
id: request.poiId,
|
||||
name: request.name || '目标位置',
|
||||
floorId: request.floorId,
|
||||
primaryCategory: 'target_preview',
|
||||
primaryCategoryZh: request.primaryCategoryZh || '位置预览',
|
||||
iconType: 'target_preview',
|
||||
positionGltf: request.positionGltf
|
||||
}
|
||||
}
|
||||
|
||||
const addFocusMarkerFromRequest = (request: TargetPoiFocusRequest) => {
|
||||
if (!poiGroup || findPoiSprite(request.poiId)) return findLoadedPoi(request.poiId) || null
|
||||
|
||||
const poi = createPoiFromFocusRequest(request)
|
||||
if (!poi?.positionGltf) return null
|
||||
|
||||
const [x, y, z] = poi.positionGltf
|
||||
const markerSize = getPoiMarkerSize()
|
||||
const sprite = new THREE.Sprite(createPoiMaterial(poi))
|
||||
sprite.position.set(x, y + markerSize * 0.5, z)
|
||||
sprite.userData.baseScale = markerSize
|
||||
sprite.userData.poi = poi
|
||||
setPoiSpriteFocusStyle(sprite, poi.id === activeFocusPoiId.value)
|
||||
poiGroup.add(sprite)
|
||||
|
||||
return poi
|
||||
}
|
||||
|
||||
const loadFloorPOIs = async (floor: FloorIndexItem) => {
|
||||
if (!poiGroup) return
|
||||
|
||||
const data = await fetchJson<FloorPoiData>(assetUrl(floor.poiDataAsset))
|
||||
const validPois = data.pois.filter((poi) => Array.isArray(poi.positionGltf) && poi.positionGltf.length === 3)
|
||||
const markerSize = activeModel ? Math.max(new THREE.Box3().setFromObject(activeModel).getSize(new THREE.Vector3()).length() * 0.012, 2.4) : 3
|
||||
const markerSize = getPoiMarkerSize()
|
||||
|
||||
validPois.forEach((poi) => {
|
||||
const [x, y, z] = poi.positionGltf!
|
||||
const sprite = new THREE.Sprite(createPoiMaterial(poi))
|
||||
sprite.position.set(x, y + markerSize * 0.5, z)
|
||||
sprite.scale.set(markerSize, markerSize, markerSize)
|
||||
sprite.renderOrder = 10
|
||||
sprite.userData.baseScale = markerSize
|
||||
sprite.userData.poi = poi
|
||||
setPoiSpriteFocusStyle(sprite, poi.id === activeFocusPoiId.value)
|
||||
poiGroup!.add(sprite)
|
||||
})
|
||||
}
|
||||
@@ -495,10 +590,134 @@ const handlePointerDown = (event: PointerEvent) => {
|
||||
|
||||
if (hits[0]?.object.userData.poi) {
|
||||
selectedPOI.value = hits[0].object.userData.poi as CleanPoi
|
||||
activeFocusPoiId.value = selectedPOI.value.id
|
||||
updatePoiMarkerFocus()
|
||||
emit('poiClick', selectedPOI.value)
|
||||
}
|
||||
}
|
||||
|
||||
const emitTargetFocus = (
|
||||
request: TargetPoiFocusRequest,
|
||||
status: TargetPoiFocusResult['status'],
|
||||
message?: string
|
||||
) => {
|
||||
emit('targetFocus', {
|
||||
requestId: request.requestId,
|
||||
poiId: request.poiId,
|
||||
floorId: request.floorId,
|
||||
status,
|
||||
message
|
||||
})
|
||||
}
|
||||
|
||||
const clearTargetFocus = () => {
|
||||
pendingTargetFocus = null
|
||||
activeFocusPoiId.value = ''
|
||||
selectedPOI.value = null
|
||||
updatePoiMarkerFocus()
|
||||
}
|
||||
|
||||
const isSceneReadyForTargetFocus = () => (
|
||||
Boolean(scene && camera && controls && loader && floorIndex.value.length)
|
||||
&& !isLoading.value
|
||||
&& !loadError.value
|
||||
)
|
||||
|
||||
const focusCameraOnPoi = (poi: CleanPoi) => {
|
||||
if (!camera || !controls || !poi.positionGltf) return
|
||||
|
||||
const [x, y, z] = poi.positionGltf
|
||||
const target = new THREE.Vector3(x, y, z)
|
||||
const modelSize = activeModel
|
||||
? new THREE.Box3().setFromObject(activeModel).getSize(new THREE.Vector3())
|
||||
: new THREE.Vector3(80, 80, 80)
|
||||
const maxDim = Math.max(modelSize.x, modelSize.y, modelSize.z, 1)
|
||||
const distance = Math.min(Math.max(maxDim * 0.22, 28), Math.max(maxDim * 0.55, 80))
|
||||
const direction = new THREE.Vector3(0.72, 0.58, 1).normalize()
|
||||
|
||||
camera.position.copy(target).add(direction.multiplyScalar(distance))
|
||||
camera.near = Math.max(0.1, maxDim / 1200)
|
||||
camera.far = Math.max(10000, maxDim * 20)
|
||||
camera.updateProjectionMatrix()
|
||||
|
||||
controls.target.copy(target)
|
||||
controls.minDistance = Math.max(2, maxDim * 0.025)
|
||||
controls.maxDistance = Math.max(20, maxDim * 6)
|
||||
controls.update()
|
||||
}
|
||||
|
||||
const focusTargetPoi = async (request: TargetPoiFocusRequest) => {
|
||||
if (!request.poiId || !request.floorId) {
|
||||
emitTargetFocus(request, 'missing', '目标位置数据不完整')
|
||||
return false
|
||||
}
|
||||
|
||||
const floor = floorIndex.value.find((item) => item.floorId === request.floorId)
|
||||
if (!floor) {
|
||||
emitTargetFocus(request, 'missing', '目标楼层不在当前三维资源中')
|
||||
return false
|
||||
}
|
||||
|
||||
activeFocusPoiId.value = request.poiId
|
||||
|
||||
try {
|
||||
if (activeView.value !== 'floor' || currentFloor.value !== request.floorId) {
|
||||
isLoading.value = true
|
||||
loadError.value = false
|
||||
await loadFloor(request.floorId)
|
||||
isLoading.value = false
|
||||
emit('floorChange', request.floorId)
|
||||
} else if (shouldRenderPoiMarkers.value && poiGroup && poiGroup.children.length === 0) {
|
||||
await loadFloorPOIs(floor)
|
||||
}
|
||||
|
||||
const poi = findLoadedPoi(request.poiId) || addFocusMarkerFromRequest(request) || createPoiFromFocusRequest(request)
|
||||
|
||||
if (!poi?.positionGltf) {
|
||||
selectedPOI.value = null
|
||||
updatePoiMarkerFocus()
|
||||
emitTargetFocus(request, 'missing', '目标暂无三维坐标')
|
||||
return false
|
||||
}
|
||||
|
||||
selectedPOI.value = poi
|
||||
updatePoiMarkerFocus()
|
||||
focusCameraOnPoi(poi)
|
||||
emitTargetFocus(request, 'focused')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('目标 POI 聚焦失败:', error)
|
||||
loadError.value = true
|
||||
isLoading.value = false
|
||||
setProgress(0, error instanceof Error ? error.message : '目标位置聚焦失败')
|
||||
emitTargetFocus(request, 'error', error instanceof Error ? error.message : '目标位置聚焦失败')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const queueTargetFocus = (request: TargetPoiFocusRequest | null) => {
|
||||
if (!request) {
|
||||
clearTargetFocus()
|
||||
return
|
||||
}
|
||||
|
||||
pendingTargetFocus = request
|
||||
|
||||
if (!isSceneReadyForTargetFocus()) return
|
||||
|
||||
const nextRequest = pendingTargetFocus
|
||||
if (!nextRequest) return
|
||||
|
||||
pendingTargetFocus = null
|
||||
targetFocusQueue = targetFocusQueue
|
||||
.then(() => focusTargetPoi(nextRequest))
|
||||
.finally(() => {
|
||||
if (pendingTargetFocus) {
|
||||
queueTargetFocus(pendingTargetFocus)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const loadCleanPackage = async () => {
|
||||
setProgress(8, '正在读取室内导览资源...')
|
||||
manifest = await fetchJson<CleanNavManifest>(assetUrl('app_nav_manifest.json'))
|
||||
@@ -537,6 +756,7 @@ const init3DScene = async () => {
|
||||
|
||||
setProgress(100, '室内三维模型加载完成')
|
||||
isLoading.value = false
|
||||
queueTargetFocus(pendingTargetFocus || props.targetFocus || null)
|
||||
} catch (error) {
|
||||
console.error('室内 3D clean package 加载失败:', error)
|
||||
loadError.value = true
|
||||
@@ -564,6 +784,7 @@ const showOverview = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
loadError.value = false
|
||||
activeFocusPoiId.value = ''
|
||||
await loadOverview()
|
||||
isLoading.value = false
|
||||
} catch (error) {
|
||||
@@ -613,8 +834,13 @@ const disposeScene = () => {
|
||||
defineExpose({
|
||||
switchFloor: handleFloorChange,
|
||||
showOverview,
|
||||
focusTargetPoi: (request: TargetPoiFocusRequest) => {
|
||||
queueTargetFocus(request)
|
||||
},
|
||||
clearNavigation: () => {
|
||||
activeFocusPoiId.value = ''
|
||||
selectedPOI.value = null
|
||||
updatePoiMarkerFocus()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -622,6 +848,13 @@ watch(() => props.assetBaseUrl, () => {
|
||||
init3DScene()
|
||||
})
|
||||
|
||||
watch(() => props.targetFocus, (request) => {
|
||||
queueTargetFocus(request || null)
|
||||
}, {
|
||||
deep: true,
|
||||
immediate: true
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
init3DScene()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user