优化导览预览与讲解展示层
This commit is contained in:
@@ -64,6 +64,7 @@ import type {
|
||||
|
||||
type ViewMode = 'overview' | 'floor' | 'multi'
|
||||
type CameraPreset = 'top' | 'oblique'
|
||||
type TouchGestureMode = 'orbit' | 'pan'
|
||||
|
||||
type FloorIndexItem = GuideModelFloorAsset
|
||||
|
||||
@@ -79,12 +80,23 @@ type PoiVisibilityTier = 'tight' | 'balanced' | 'full'
|
||||
interface PoiSpriteUserData {
|
||||
poi?: RenderPoi
|
||||
baseScale?: number
|
||||
labelBaseScaleX?: number
|
||||
labelBaseScaleY?: number
|
||||
isPoiLabel?: boolean
|
||||
isPoiPulse?: boolean
|
||||
isPoiBase?: boolean
|
||||
isCorePoi?: boolean
|
||||
}
|
||||
|
||||
interface FocusHallMaterialState {
|
||||
material: THREE.Material
|
||||
color?: THREE.Color
|
||||
emissive?: THREE.Color
|
||||
emissiveIntensity?: number
|
||||
opacity: number
|
||||
transparent: boolean
|
||||
}
|
||||
|
||||
interface TargetPoiFocusRequest {
|
||||
requestId: number | string
|
||||
poiId: string
|
||||
@@ -93,6 +105,7 @@ interface TargetPoiFocusRequest {
|
||||
floorLabel?: string
|
||||
primaryCategoryZh?: string
|
||||
positionGltf?: [number, number, number]
|
||||
sourceObjectName?: string
|
||||
}
|
||||
|
||||
interface TargetPoiFocusResult {
|
||||
@@ -120,6 +133,8 @@ const props = withDefaults(defineProps<{
|
||||
showControls?: boolean
|
||||
showPoi?: boolean
|
||||
targetFocus?: TargetPoiFocusRequest | null
|
||||
touchGestureMode?: TouchGestureMode
|
||||
targetFocusDistanceFactor?: number
|
||||
routePreview?: GuideRouteResult | null
|
||||
showRoute?: boolean
|
||||
autoSwitch?: boolean
|
||||
@@ -133,6 +148,8 @@ const props = withDefaults(defineProps<{
|
||||
showControls: true,
|
||||
showPoi: false,
|
||||
targetFocus: null,
|
||||
touchGestureMode: 'orbit',
|
||||
targetFocusDistanceFactor: 0.22,
|
||||
routePreview: null,
|
||||
showRoute: false,
|
||||
autoSwitch: true,
|
||||
@@ -183,6 +200,8 @@ let routeGroup: THREE.Group | null = null
|
||||
let activeFocusLabelSprite: THREE.Sprite | null = null
|
||||
let activeFocusPulseSprite: THREE.Sprite | null = null
|
||||
let activeFocusBaseSprite: THREE.Sprite | null = null
|
||||
let activeFocusHallGlowMesh: THREE.Mesh | null = null
|
||||
let activeFocusHallMaterialStates: FocusHallMaterialState[] = []
|
||||
let animationId = 0
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let isDisposed = false
|
||||
@@ -201,6 +220,15 @@ let autoSwitchDisableTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let isProgrammaticCameraChange = false
|
||||
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const syncControlInteractionOptions = () => {
|
||||
if (!controls) return
|
||||
|
||||
const shouldEnablePan = props.showControls || props.touchGestureMode === 'pan'
|
||||
controls.enablePan = shouldEnablePan
|
||||
controls.touches.ONE = props.touchGestureMode === 'pan' ? THREE.TOUCH.PAN : THREE.TOUCH.ROTATE
|
||||
controls.touches.TWO = THREE.TOUCH.DOLLY_PAN
|
||||
}
|
||||
|
||||
const corePoiCategories = new Set([
|
||||
'basic_service_facility',
|
||||
'transport_circulation',
|
||||
@@ -276,6 +304,61 @@ const getModelNodeNames = (object: THREE.Object3D) => {
|
||||
return names
|
||||
}
|
||||
|
||||
const normalizeModelMatchKey = (value: string) => (
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_\-./\\]/g, '')
|
||||
)
|
||||
|
||||
const getPoiModelMatchKeys = (poi: RenderPoi) => (
|
||||
[
|
||||
poi.sourceObjectName,
|
||||
`${poi.floorId}_${poi.name}`,
|
||||
poi.name
|
||||
]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map(normalizeModelMatchKey)
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
const isPoiModelNodeMatch = (object: THREE.Object3D, poi: RenderPoi) => {
|
||||
const keys = getPoiModelMatchKeys(poi)
|
||||
if (!keys.length) return false
|
||||
|
||||
return getModelNodeNames(object)
|
||||
.map(normalizeModelMatchKey)
|
||||
.some((name) => keys.some((key) => name === key || name.includes(key)))
|
||||
}
|
||||
|
||||
const findPoiModelRoot = (poi: RenderPoi): THREE.Object3D | null => {
|
||||
if (!activeModel) return null
|
||||
|
||||
let matchedRoot: THREE.Object3D | null = null
|
||||
|
||||
activeModel.traverse((child) => {
|
||||
if (matchedRoot || child === activeModel) return
|
||||
|
||||
const childName = child.name ? normalizeModelMatchKey(child.name) : ''
|
||||
const keys = getPoiModelMatchKeys(poi)
|
||||
if (childName && keys.some((key) => childName === key || childName.includes(key))) {
|
||||
matchedRoot = child
|
||||
}
|
||||
})
|
||||
|
||||
if (matchedRoot) return matchedRoot
|
||||
|
||||
activeModel.traverse((child) => {
|
||||
if (matchedRoot || !(child instanceof THREE.Mesh) || !child.visible) return
|
||||
|
||||
if (isPoiModelNodeMatch(child, poi)) {
|
||||
matchedRoot = child.parent && child.parent !== activeModel ? child.parent : child
|
||||
}
|
||||
})
|
||||
|
||||
return matchedRoot
|
||||
}
|
||||
|
||||
const isExteriorModelNode = (object: THREE.Object3D) => (
|
||||
getModelNodeNames(object).some((name) => (
|
||||
floorExteriorNameKeywords.some((keyword) => name.includes(keyword))
|
||||
@@ -591,12 +674,12 @@ const initThree = async () => {
|
||||
controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.08
|
||||
controls.enablePan = props.showControls
|
||||
controls.enableZoom = true
|
||||
controls.minDistance = 6
|
||||
controls.maxDistance = 1200
|
||||
controls.minPolarAngle = 0.08
|
||||
controls.maxPolarAngle = Math.PI * 0.48
|
||||
syncControlInteractionOptions()
|
||||
|
||||
loader = new GLTFLoader()
|
||||
poiGroup = new THREE.Group()
|
||||
@@ -636,6 +719,7 @@ const startRenderLoop = () => {
|
||||
if (isDisposed || !renderer || !scene || !camera) return
|
||||
|
||||
controls?.update()
|
||||
updateFocusLabelScale()
|
||||
renderer.render(scene, camera)
|
||||
animationId = window.requestAnimationFrame(render)
|
||||
}
|
||||
@@ -675,6 +759,174 @@ const disposeObject = (object: THREE.Object3D) => {
|
||||
})
|
||||
}
|
||||
|
||||
const getMaterialColor = (material: THREE.Material) => {
|
||||
const candidate = material as THREE.Material & { color?: unknown }
|
||||
return candidate.color instanceof THREE.Color ? candidate.color : undefined
|
||||
}
|
||||
|
||||
const getMaterialEmissive = (material: THREE.Material) => {
|
||||
const candidate = material as THREE.Material & { emissive?: unknown }
|
||||
return candidate.emissive instanceof THREE.Color ? candidate.emissive : undefined
|
||||
}
|
||||
|
||||
const getMaterialEmissiveIntensity = (material: THREE.Material) => {
|
||||
const candidate = material as THREE.Material & { emissiveIntensity?: unknown }
|
||||
return typeof candidate.emissiveIntensity === 'number'
|
||||
? candidate.emissiveIntensity
|
||||
: undefined
|
||||
}
|
||||
|
||||
const setMaterialEmissiveIntensity = (material: THREE.Material, value: number) => {
|
||||
const candidate = material as THREE.Material & { emissiveIntensity?: number }
|
||||
if (typeof candidate.emissiveIntensity === 'number') {
|
||||
candidate.emissiveIntensity = value
|
||||
}
|
||||
}
|
||||
|
||||
const clearFocusHallHighlight = () => {
|
||||
activeFocusHallMaterialStates.forEach((state) => {
|
||||
const color = getMaterialColor(state.material)
|
||||
const emissive = getMaterialEmissive(state.material)
|
||||
|
||||
if (color && state.color) {
|
||||
color.copy(state.color)
|
||||
}
|
||||
|
||||
if (emissive && state.emissive) {
|
||||
emissive.copy(state.emissive)
|
||||
}
|
||||
|
||||
if (typeof state.emissiveIntensity === 'number') {
|
||||
setMaterialEmissiveIntensity(state.material, state.emissiveIntensity)
|
||||
}
|
||||
|
||||
state.material.opacity = state.opacity
|
||||
state.material.transparent = state.transparent
|
||||
state.material.needsUpdate = true
|
||||
})
|
||||
|
||||
activeFocusHallMaterialStates = []
|
||||
|
||||
if (activeFocusHallGlowMesh) {
|
||||
activeFocusHallGlowMesh.parent?.remove(activeFocusHallGlowMesh)
|
||||
const material = activeFocusHallGlowMesh.material
|
||||
if (!Array.isArray(material)) {
|
||||
const glowMaterial = material as THREE.MeshBasicMaterial
|
||||
glowMaterial.map?.dispose()
|
||||
}
|
||||
disposeObject(activeFocusHallGlowMesh)
|
||||
activeFocusHallGlowMesh = null
|
||||
}
|
||||
}
|
||||
|
||||
const applyFocusHallMaterial = (material: THREE.Material, seenMaterials: Set<THREE.Material>) => {
|
||||
if (seenMaterials.has(material)) return
|
||||
seenMaterials.add(material)
|
||||
|
||||
const color = getMaterialColor(material)
|
||||
const emissive = getMaterialEmissive(material)
|
||||
const emissiveIntensity = getMaterialEmissiveIntensity(material)
|
||||
|
||||
activeFocusHallMaterialStates.push({
|
||||
material,
|
||||
color: color?.clone(),
|
||||
emissive: emissive?.clone(),
|
||||
emissiveIntensity,
|
||||
opacity: material.opacity,
|
||||
transparent: material.transparent
|
||||
})
|
||||
|
||||
const glowColor = new THREE.Color('#e0df00')
|
||||
|
||||
if (color) {
|
||||
color.lerp(glowColor, 0.34)
|
||||
}
|
||||
|
||||
if (emissive) {
|
||||
emissive.copy(glowColor)
|
||||
setMaterialEmissiveIntensity(material, Math.max(emissiveIntensity || 0, 0.76))
|
||||
}
|
||||
|
||||
material.opacity = Math.max(material.opacity, 0.96)
|
||||
material.transparent = true
|
||||
material.needsUpdate = true
|
||||
}
|
||||
|
||||
const createFocusHallGlowMesh = (poi: RenderPoi) => {
|
||||
if (!poi.positionGltf) return null
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 256
|
||||
canvas.height = 256
|
||||
const context = canvas.getContext('2d')
|
||||
|
||||
if (context) {
|
||||
const gradient = context.createRadialGradient(128, 128, 16, 128, 128, 118)
|
||||
gradient.addColorStop(0, 'rgba(224, 223, 0, 0.52)')
|
||||
gradient.addColorStop(0.45, 'rgba(224, 223, 0, 0.34)')
|
||||
gradient.addColorStop(0.78, 'rgba(224, 223, 0, 0.16)')
|
||||
gradient.addColorStop(1, 'rgba(224, 223, 0, 0)')
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||
context.fillStyle = gradient
|
||||
context.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas)
|
||||
texture.colorSpace = THREE.SRGBColorSpace
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: 0.78,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
const hallGlowSize = Math.max(getPoiMarkerSize() * 14, getActiveModelSpan() * 0.14)
|
||||
const geometry = new THREE.PlaneGeometry(hallGlowSize, hallGlowSize * 0.72)
|
||||
const mesh = new THREE.Mesh(geometry, material)
|
||||
const [x, y, z] = poi.positionGltf
|
||||
|
||||
mesh.name = 'GuideFocusHallGlow'
|
||||
mesh.position.set(x, y + 0.08, z)
|
||||
mesh.rotation.x = -Math.PI / 2
|
||||
mesh.renderOrder = 7
|
||||
|
||||
return mesh
|
||||
}
|
||||
|
||||
const showFocusHallHighlight = (poi: RenderPoi) => {
|
||||
clearFocusHallHighlight()
|
||||
if (!activeModel) return
|
||||
|
||||
activeFocusHallGlowMesh = createFocusHallGlowMesh(poi)
|
||||
if (activeFocusHallGlowMesh) {
|
||||
poiGroup?.add(activeFocusHallGlowMesh)
|
||||
}
|
||||
|
||||
const modelRoot = findPoiModelRoot(poi)
|
||||
if (!modelRoot) return
|
||||
|
||||
const seenMaterials = new Set<THREE.Material>()
|
||||
let matchedMeshCount = 0
|
||||
|
||||
modelRoot.traverse((child) => {
|
||||
if (!(child instanceof THREE.Mesh) || !child.visible) return
|
||||
|
||||
matchedMeshCount += 1
|
||||
const materials = Array.isArray(child.material) ? child.material : [child.material]
|
||||
materials.forEach((material) => {
|
||||
if (material) {
|
||||
applyFocusHallMaterial(material, seenMaterials)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!matchedMeshCount) {
|
||||
clearFocusHallHighlight()
|
||||
}
|
||||
}
|
||||
|
||||
const disposeCachedOverviewModel = () => {
|
||||
if (!cachedOverviewModel) return
|
||||
|
||||
@@ -840,6 +1092,7 @@ const renderRoutePreview = () => {
|
||||
|
||||
const clearSceneData = () => {
|
||||
if (activeModel && scene) {
|
||||
clearFocusHallHighlight()
|
||||
scene.remove(activeModel)
|
||||
if (activeModel === cachedOverviewModel) {
|
||||
cachedOverviewModel.visible = false
|
||||
@@ -1390,8 +1643,10 @@ const createPoiLabelSprite = (poi: RenderPoi, markerSize: number) => {
|
||||
depthWrite: false
|
||||
}))
|
||||
sprite.userData.isPoiLabel = true
|
||||
sprite.userData.labelBaseScaleX = markerSize * 5.6
|
||||
sprite.userData.labelBaseScaleY = markerSize * 1.68
|
||||
sprite.renderOrder = 30
|
||||
sprite.scale.set(markerSize * 5.6, markerSize * 1.68, 1)
|
||||
sprite.scale.set(sprite.userData.labelBaseScaleX, sprite.userData.labelBaseScaleY, 1)
|
||||
|
||||
return sprite
|
||||
}
|
||||
@@ -1501,6 +1756,7 @@ const showFocusPoiLabel = (poi: RenderPoi) => {
|
||||
const [x, y, z] = poi.positionGltf
|
||||
activeFocusLabelSprite = createPoiLabelSprite(poi, markerSize)
|
||||
activeFocusLabelSprite.position.set(x, y + markerSize * 2.35, z)
|
||||
updateFocusLabelScale()
|
||||
poiGroup.add(activeFocusLabelSprite)
|
||||
}
|
||||
|
||||
@@ -1529,6 +1785,7 @@ const showFocusPoiPulse = (poi: RenderPoi) => {
|
||||
}
|
||||
|
||||
const showFocusPoiAffordances = (poi: RenderPoi) => {
|
||||
showFocusHallHighlight(poi)
|
||||
showFocusPoiBase(poi)
|
||||
showFocusPoiPulse(poi)
|
||||
showFocusPoiLabel(poi)
|
||||
@@ -1552,6 +1809,28 @@ const getPoiMarkerSize = () => (
|
||||
: 3
|
||||
)
|
||||
|
||||
const getFocusLabelScaleBoost = () => {
|
||||
if (!controls || !activeModel) return 1
|
||||
|
||||
const distanceRatio = controls.getDistance() / getActiveModelSpan()
|
||||
return Math.min(2.25, Math.max(1, distanceRatio / 0.36))
|
||||
}
|
||||
|
||||
const updateFocusLabelScale = () => {
|
||||
if (!activeFocusLabelSprite) return
|
||||
|
||||
const userData = getPoiSpriteUserData(activeFocusLabelSprite)
|
||||
const baseScaleX = typeof userData.labelBaseScaleX === 'number'
|
||||
? userData.labelBaseScaleX
|
||||
: activeFocusLabelSprite.scale.x
|
||||
const baseScaleY = typeof userData.labelBaseScaleY === 'number'
|
||||
? userData.labelBaseScaleY
|
||||
: activeFocusLabelSprite.scale.y
|
||||
const scaleBoost = getFocusLabelScaleBoost()
|
||||
|
||||
activeFocusLabelSprite.scale.set(baseScaleX * scaleBoost, baseScaleY * scaleBoost, 1)
|
||||
}
|
||||
|
||||
const setPoiSpriteFocusStyle = (sprite: THREE.Sprite, focused: boolean) => {
|
||||
const userData = getPoiSpriteUserData(sprite)
|
||||
const baseScale = typeof userData.baseScale === 'number'
|
||||
@@ -1603,7 +1882,8 @@ const createPoiFromFocusRequest = (request: TargetPoiFocusRequest): RenderPoi |
|
||||
primaryCategory: 'target_preview',
|
||||
primaryCategoryZh: request.primaryCategoryZh || '位置预览',
|
||||
iconType: 'target_preview',
|
||||
positionGltf: request.positionGltf
|
||||
positionGltf: request.positionGltf,
|
||||
sourceObjectName: request.sourceObjectName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1654,6 +1934,7 @@ const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
|
||||
const clearMapSelection = (shouldEmit = true) => {
|
||||
activeFocusPoiId.value = ''
|
||||
selectedPOI.value = null
|
||||
clearFocusHallHighlight()
|
||||
disposeFocusLabel()
|
||||
disposeFocusPulse()
|
||||
disposeFocusBase()
|
||||
@@ -1779,7 +2060,12 @@ const focusCameraOnPoi = (poi: RenderPoi) => {
|
||||
? 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 focusDistanceFactor = Math.max(props.targetFocusDistanceFactor, 0.1)
|
||||
const focusMaxDistanceFactor = Math.max(0.55, focusDistanceFactor * 1.4)
|
||||
const distance = Math.min(
|
||||
Math.max(maxDim * focusDistanceFactor, 28),
|
||||
Math.max(maxDim * focusMaxDistanceFactor, 80)
|
||||
)
|
||||
const direction = new THREE.Vector3(0.72, 0.58, 1).normalize()
|
||||
|
||||
camera.position.copy(target).add(direction.multiplyScalar(distance))
|
||||
@@ -2036,6 +2322,10 @@ watch(() => props.modelSource, () => {
|
||||
init3DScene()
|
||||
})
|
||||
|
||||
watch(() => [props.showControls, props.touchGestureMode], () => {
|
||||
syncControlInteractionOptions()
|
||||
})
|
||||
|
||||
watch(() => props.targetFocus, (request) => {
|
||||
queueTargetFocus(request || null)
|
||||
}, {
|
||||
|
||||
Reference in New Issue
Block a user