@@ -74,6 +74,7 @@ type ViewMode = 'overview' | 'floor' | 'multi'
|
|||||||
type CameraPreset = 'top' | 'oblique'
|
type CameraPreset = 'top' | 'oblique'
|
||||||
type TouchGestureMode = 'orbit' | 'pan'
|
type TouchGestureMode = 'orbit' | 'pan'
|
||||||
type ZoomCameraSource = 'button' | 'gesture'
|
type ZoomCameraSource = 'button' | 'gesture'
|
||||||
|
type InteractionPolicyMode = 'display' | 'explore'
|
||||||
|
|
||||||
interface InitialModelProgressEvent {
|
interface InitialModelProgressEvent {
|
||||||
progress: number
|
progress: number
|
||||||
@@ -132,6 +133,34 @@ const SGS_VISUAL_RENDER_CONFIG = {
|
|||||||
}
|
}
|
||||||
} as const
|
} 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 {
|
interface CameraFitOptions {
|
||||||
distanceFactor?: number
|
distanceFactor?: number
|
||||||
targetOffsetRatio?: THREE.Vector3
|
targetOffsetRatio?: THREE.Vector3
|
||||||
@@ -366,6 +395,16 @@ let floorSwitchRequestedFloorId = ''
|
|||||||
let isFloorSwitching = false
|
let isFloorSwitching = false
|
||||||
let hasLoadedFloorViewOnce = false
|
let hasLoadedFloorViewOnce = false
|
||||||
let pointerDownState: { x: number; y: number } | null = null
|
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 cachedOverviewModel: THREE.Object3D | null = null
|
||||||
let cachedSharedModelUrl = ''
|
let cachedSharedModelUrl = ''
|
||||||
let autoSwitchOverviewMaxDim = 0
|
let autoSwitchOverviewMaxDim = 0
|
||||||
@@ -494,18 +533,89 @@ const logThreeMapDiagnostic = (
|
|||||||
console.debug(`[ThreeMap] ${event} ${stringifyDiagnosticPayload(payload)}`)
|
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
|
if (!controls) return
|
||||||
|
|
||||||
controls.enableRotate = false
|
const policy = INTERACTION_POLICY[getInteractionPolicyMode()]
|
||||||
controls.enablePan = true
|
const enableRotate = shouldEnableRotation()
|
||||||
|
const enablePan = policy.allowPan && !enableRotate
|
||||||
|
|
||||||
|
controls.enableRotate = enableRotate
|
||||||
|
controls.enablePan = enablePan
|
||||||
|
controls.enableZoom = policy.allowZoom
|
||||||
controls.screenSpacePanning = true
|
controls.screenSpacePanning = true
|
||||||
controls.panSpeed = 0.85
|
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.MIDDLE = THREE.MOUSE.DOLLY
|
||||||
controls.mouseButtons.RIGHT = THREE.MOUSE.PAN
|
controls.mouseButtons.RIGHT = enablePan ? THREE.MOUSE.PAN : null
|
||||||
controls.touches.ONE = THREE.TOUCH.PAN
|
controls.touches.ONE = enablePan ? THREE.TOUCH.PAN : null
|
||||||
controls.touches.TWO = THREE.TOUCH.DOLLY_PAN
|
controls.touches.TWO = THREE.TOUCH.DOLLY_ROTATE
|
||||||
}
|
}
|
||||||
|
|
||||||
const corePoiCategories = new Set([
|
const corePoiCategories = new Set([
|
||||||
@@ -1733,8 +1843,14 @@ const initThree = async () => {
|
|||||||
|
|
||||||
resizeObserver = new ResizeObserver(handleResize)
|
resizeObserver = new ResizeObserver(handleResize)
|
||||||
resizeObserver.observe(container)
|
resizeObserver.observe(container)
|
||||||
container.addEventListener('pointerdown', handlePointerDown)
|
container.addEventListener('pointerdown', handlePointerDown, true)
|
||||||
container.addEventListener('pointerup', handlePointerUp)
|
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 })
|
renderer.domElement.addEventListener('wheel', handleWheelIntent, { passive: true, capture: true })
|
||||||
|
|
||||||
// 监听控制器变化,用于自动切换和点位可见层级更新
|
// 监听控制器变化,用于自动切换和点位可见层级更新
|
||||||
@@ -3361,6 +3477,7 @@ const loadOverview = async () => {
|
|||||||
|
|
||||||
const loadToken = startModelLoad()
|
const loadToken = startModelLoad()
|
||||||
activeView.value = 'overview'
|
activeView.value = 'overview'
|
||||||
|
syncControlInteractionOptions()
|
||||||
clearSceneData()
|
clearSceneData()
|
||||||
|
|
||||||
if (cachedOverviewModel) {
|
if (cachedOverviewModel) {
|
||||||
@@ -3506,6 +3623,7 @@ const commitPreparedFloorScene = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeView.value = 'floor'
|
activeView.value = 'floor'
|
||||||
|
syncControlInteractionOptions()
|
||||||
currentFloor.value = expectedFloorId
|
currentFloor.value = expectedFloorId
|
||||||
clearSceneData()
|
clearSceneData()
|
||||||
activeModel = prepared.model
|
activeModel = prepared.model
|
||||||
@@ -3588,6 +3706,7 @@ const loadMultiFloor = async () => {
|
|||||||
|
|
||||||
const loadToken = startModelLoad()
|
const loadToken = startModelLoad()
|
||||||
activeView.value = 'multi'
|
activeView.value = 'multi'
|
||||||
|
syncControlInteractionOptions()
|
||||||
clearSceneData()
|
clearSceneData()
|
||||||
setProgress(18, '正在加载多层展示模型...')
|
setProgress(18, '正在加载多层展示模型...')
|
||||||
|
|
||||||
@@ -4550,6 +4669,22 @@ const handleSceneTap = async (event: PointerEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerDown = (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 = {
|
pointerDownState = {
|
||||||
x: event.clientX,
|
x: event.clientX,
|
||||||
y: event.clientY
|
y: event.clientY
|
||||||
@@ -4557,6 +4692,18 @@ const handlePointerDown = (event: PointerEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerUp = (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
|
if (!pointerDownState) return
|
||||||
|
|
||||||
const moveDistance = Math.hypot(event.clientX - pointerDownState.x, event.clientY - pointerDownState.y)
|
const moveDistance = Math.hypot(event.clientX - pointerDownState.x, event.clientY - pointerDownState.y)
|
||||||
@@ -4567,6 +4714,43 @@ const handlePointerUp = (event: PointerEvent) => {
|
|||||||
void handleSceneTap(event)
|
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 = (
|
const emitTargetFocus = (
|
||||||
request: TargetPoiFocusRequest,
|
request: TargetPoiFocusRequest,
|
||||||
status: TargetPoiFocusResult['status'],
|
status: TargetPoiFocusResult['status'],
|
||||||
@@ -4874,8 +5058,14 @@ const disposeScene = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const container = getContainerElement()
|
const container = getContainerElement()
|
||||||
container?.removeEventListener('pointerdown', handlePointerDown)
|
container?.removeEventListener('pointerdown', handlePointerDown, true)
|
||||||
container?.removeEventListener('pointerup', handlePointerUp)
|
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)
|
renderer?.domElement.removeEventListener('wheel', handleWheelIntent, true)
|
||||||
|
|
||||||
resizeObserver?.disconnect()
|
resizeObserver?.disconnect()
|
||||||
@@ -4901,6 +5091,7 @@ const disposeScene = () => {
|
|||||||
isProgrammaticCameraChange = false
|
isProgrammaticCameraChange = false
|
||||||
lastControlDistance = 0
|
lastControlDistance = 0
|
||||||
activeControlGestureStartedAt = 0
|
activeControlGestureStartedAt = 0
|
||||||
|
resetInteractionGateState()
|
||||||
indoorEntryGestureLocked = false
|
indoorEntryGestureLocked = false
|
||||||
indoorContextLockedUntil = 0
|
indoorContextLockedUntil = 0
|
||||||
resetOverviewEntryIntent()
|
resetOverviewEntryIntent()
|
||||||
|
|||||||
Reference in New Issue
Block a user