From a5af007b203cd3da966ac7ef12f614a4735cf91b Mon Sep 17 00:00:00 2001 From: lyf <2514544224@qq.com> Date: Thu, 9 Jul 2026 01:41:03 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=B8=89=E7=BB=B4=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E4=BA=A4=E4=BA=92=E9=97=A8=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/map/ThreeMap.vue | 213 ++++++++++++++++++++++++++++++-- 1 file changed, 202 insertions(+), 11 deletions(-) diff --git a/src/components/map/ThreeMap.vue b/src/components/map/ThreeMap.vue index da8ecd8..14a9201 100644 --- a/src/components/map/ThreeMap.vue +++ b/src/components/map/ThreeMap.vue @@ -74,6 +74,7 @@ type ViewMode = 'overview' | 'floor' | 'multi' type CameraPreset = 'top' | 'oblique' type TouchGestureMode = 'orbit' | 'pan' type ZoomCameraSource = 'button' | 'gesture' +type InteractionPolicyMode = 'display' | 'explore' interface InitialModelProgressEvent { progress: number @@ -132,6 +133,34 @@ const SGS_VISUAL_RENDER_CONFIG = { } } as const +const degToRad = (degrees: number) => THREE.MathUtils.degToRad(degrees) + +const INTERACTION_POLICY = { + display: { + minPolarAngle: degToRad(35), + maxPolarAngle: degToRad(76), + minAzimuthAngle: degToRad(-75), + maxAzimuthAngle: degToRad(75), + allowPan: true, + allowZoom: true + }, + explore: { + minPolarAngle: degToRad(30), + maxPolarAngle: degToRad(78), + minAzimuthAngle: degToRad(-90), + maxAzimuthAngle: degToRad(90), + allowPan: true, + allowZoom: true + } +} as const satisfies Record + interface CameraFitOptions { distanceFactor?: number targetOffsetRatio?: THREE.Vector3 @@ -366,6 +395,16 @@ let floorSwitchRequestedFloorId = '' let isFloorSwitching = false let hasLoadedFloorViewOnce = false let pointerDownState: { x: number; y: number } | null = null +const activePointers = new Set() +let activePointerTypes = new Map() +let hadMultiPointerGesture = false +let isDesktopRotateModifierActive = false +let desktopRotateModifiers = { + shift: false, + ctrl: false, + meta: false, + alt: false +} let cachedOverviewModel: THREE.Object3D | null = null let cachedSharedModelUrl = '' let autoSwitchOverviewMaxDim = 0 @@ -494,18 +533,89 @@ const logThreeMapDiagnostic = ( console.debug(`[ThreeMap] ${event} ${stringifyDiagnosticPayload(payload)}`) } -const syncControlInteractionOptions = () => { +const getInteractionPolicyMode = (): InteractionPolicyMode => ( + activeView.value === 'overview' ? 'display' : 'explore' +) + +const getActiveTouchPointerCount = () => ( + [...activePointerTypes.values()].filter((pointerType) => pointerType === 'touch').length +) + +const updateDesktopRotateModifierState = (event?: KeyboardEvent | PointerEvent) => { + if (event) { + desktopRotateModifiers = { + shift: event.shiftKey, + ctrl: event.ctrlKey, + meta: event.metaKey, + alt: event.altKey + } + } + + isDesktopRotateModifierActive = ( + desktopRotateModifiers.shift + || desktopRotateModifiers.ctrl + || desktopRotateModifiers.meta + || desktopRotateModifiers.alt + ) +} + +const shouldEnableRotation = () => ( + getActiveTouchPointerCount() >= 2 + || isDesktopRotateModifierActive +) + +const shouldUseDirectMouseRotate = (event?: PointerEvent) => { + const hasAlt = event?.altKey ?? desktopRotateModifiers.alt + const hasShiftCtrlOrMeta = event + ? event.shiftKey || event.ctrlKey || event.metaKey + : desktopRotateModifiers.shift || desktopRotateModifiers.ctrl || desktopRotateModifiers.meta + + return hasAlt && !hasShiftCtrlOrMeta +} + +const resetInteractionGateState = () => { + activePointers.clear() + activePointerTypes.clear() + hadMultiPointerGesture = false + pointerDownState = null + desktopRotateModifiers = { + shift: false, + ctrl: false, + meta: false, + alt: false + } + isDesktopRotateModifierActive = false + syncControlInteractionOptions() +} + +const syncControlInteractionOptions = (event?: PointerEvent) => { if (!controls) return - controls.enableRotate = false - controls.enablePan = true + const policy = INTERACTION_POLICY[getInteractionPolicyMode()] + const enableRotate = shouldEnableRotation() + const enablePan = policy.allowPan && !enableRotate + + controls.enableRotate = enableRotate + controls.enablePan = enablePan + controls.enableZoom = policy.allowZoom controls.screenSpacePanning = true controls.panSpeed = 0.85 - controls.mouseButtons.LEFT = THREE.MOUSE.PAN + controls.minPolarAngle = policy.minPolarAngle + controls.maxPolarAngle = policy.maxPolarAngle + controls.minAzimuthAngle = policy.minAzimuthAngle + controls.maxAzimuthAngle = policy.maxAzimuthAngle + + controls.mouseButtons.LEFT = enableRotate + ? shouldUseDirectMouseRotate(event) + ? THREE.MOUSE.ROTATE + : THREE.MOUSE.PAN + : enablePan + ? THREE.MOUSE.PAN + : null controls.mouseButtons.MIDDLE = THREE.MOUSE.DOLLY - controls.mouseButtons.RIGHT = THREE.MOUSE.PAN - controls.touches.ONE = THREE.TOUCH.PAN - controls.touches.TWO = THREE.TOUCH.DOLLY_PAN + controls.mouseButtons.RIGHT = enablePan ? THREE.MOUSE.PAN : null + controls.touches.ONE = enablePan ? THREE.TOUCH.PAN : null + controls.touches.TWO = THREE.TOUCH.DOLLY_ROTATE } const corePoiCategories = new Set([ @@ -1733,8 +1843,14 @@ const initThree = async () => { resizeObserver = new ResizeObserver(handleResize) resizeObserver.observe(container) - container.addEventListener('pointerdown', handlePointerDown) - container.addEventListener('pointerup', handlePointerUp) + container.addEventListener('pointerdown', handlePointerDown, true) + container.addEventListener('pointerup', handlePointerUp, true) + container.addEventListener('pointercancel', handlePointerCancel, true) + container.addEventListener('pointerleave', handlePointerLeave, true) + window.addEventListener('keydown', handleKeyDown) + window.addEventListener('keyup', handleKeyUp) + window.addEventListener('blur', handleWindowBlur) + document.addEventListener('visibilitychange', handleVisibilityChange) renderer.domElement.addEventListener('wheel', handleWheelIntent, { passive: true, capture: true }) // 监听控制器变化,用于自动切换和点位可见层级更新 @@ -3361,6 +3477,7 @@ const loadOverview = async () => { const loadToken = startModelLoad() activeView.value = 'overview' + syncControlInteractionOptions() clearSceneData() if (cachedOverviewModel) { @@ -3506,6 +3623,7 @@ const commitPreparedFloorScene = ( } activeView.value = 'floor' + syncControlInteractionOptions() currentFloor.value = expectedFloorId clearSceneData() activeModel = prepared.model @@ -3588,6 +3706,7 @@ const loadMultiFloor = async () => { const loadToken = startModelLoad() activeView.value = 'multi' + syncControlInteractionOptions() clearSceneData() setProgress(18, '正在加载多层展示模型...') @@ -4550,6 +4669,22 @@ const handleSceneTap = async (event: PointerEvent) => { } const handlePointerDown = (event: PointerEvent) => { + activePointers.add(event.pointerId) + activePointerTypes.set(event.pointerId, event.pointerType) + if (event.pointerType === 'touch' && getActiveTouchPointerCount() >= 2) { + hadMultiPointerGesture = true + pointerDownState = null + } + if (event.pointerType !== 'touch') { + updateDesktopRotateModifierState(event) + } + syncControlInteractionOptions(event) + + if (event.pointerType === 'touch' && hadMultiPointerGesture) { + pointerDownState = null + return + } + pointerDownState = { x: event.clientX, y: event.clientY @@ -4557,6 +4692,18 @@ const handlePointerDown = (event: PointerEvent) => { } const handlePointerUp = (event: PointerEvent) => { + activePointers.delete(event.pointerId) + activePointerTypes.delete(event.pointerId) + syncControlInteractionOptions(event) + + if (hadMultiPointerGesture) { + if (getActiveTouchPointerCount() === 0) { + hadMultiPointerGesture = false + } + pointerDownState = null + return + } + if (!pointerDownState) return const moveDistance = Math.hypot(event.clientX - pointerDownState.x, event.clientY - pointerDownState.y) @@ -4567,6 +4714,43 @@ const handlePointerUp = (event: PointerEvent) => { void handleSceneTap(event) } +const handlePointerCancel = (event: PointerEvent) => { + activePointers.delete(event.pointerId) + activePointerTypes.delete(event.pointerId) + pointerDownState = null + if (getActiveTouchPointerCount() === 0) { + hadMultiPointerGesture = false + } + syncControlInteractionOptions(event) +} + +const handlePointerLeave = (event: PointerEvent) => { + if (event.pointerType === 'mouse') { + pointerDownState = null + } +} + +const handleKeyDown = (event: KeyboardEvent) => { + if (!event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) return + updateDesktopRotateModifierState(event) + syncControlInteractionOptions() +} + +const handleKeyUp = (event: KeyboardEvent) => { + updateDesktopRotateModifierState(event) + syncControlInteractionOptions() +} + +const handleWindowBlur = () => { + resetInteractionGateState() +} + +const handleVisibilityChange = () => { + if (document.visibilityState !== 'visible') { + resetInteractionGateState() + } +} + const emitTargetFocus = ( request: TargetPoiFocusRequest, status: TargetPoiFocusResult['status'], @@ -4874,8 +5058,14 @@ const disposeScene = () => { } const container = getContainerElement() - container?.removeEventListener('pointerdown', handlePointerDown) - container?.removeEventListener('pointerup', handlePointerUp) + container?.removeEventListener('pointerdown', handlePointerDown, true) + container?.removeEventListener('pointerup', handlePointerUp, true) + container?.removeEventListener('pointercancel', handlePointerCancel, true) + container?.removeEventListener('pointerleave', handlePointerLeave, true) + window.removeEventListener('keydown', handleKeyDown) + window.removeEventListener('keyup', handleKeyUp) + window.removeEventListener('blur', handleWindowBlur) + document.removeEventListener('visibilitychange', handleVisibilityChange) renderer?.domElement.removeEventListener('wheel', handleWheelIntent, true) resizeObserver?.disconnect() @@ -4901,6 +5091,7 @@ const disposeScene = () => { isProgrammaticCameraChange = false lastControlDistance = 0 activeControlGestureStartedAt = 0 + resetInteractionGateState() indoorEntryGestureLocked = false indoorContextLockedUntil = 0 resetOverviewEntryIntent()