chore: solidify guide P1 snapshot

This commit is contained in:
lyf
2026-06-11 16:18:57 +08:00
parent a90f63cef0
commit 9790501c3b
32 changed files with 4613 additions and 865 deletions

View File

@@ -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()
})