@@ -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<InteractionPolicyMode, {
|
||||
minPolarAngle: number
|
||||
maxPolarAngle: number
|
||||
minAzimuthAngle: number
|
||||
maxAzimuthAngle: number
|
||||
allowPan: boolean
|
||||
allowZoom: boolean
|
||||
}>
|
||||
|
||||
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<number>()
|
||||
let activePointerTypes = new Map<number, string>()
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user