优化建筑内外自动切换状态机
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-13 16:16:24 +08:00
parent e473b6a2a5
commit 9c003c16dc
5 changed files with 502 additions and 363 deletions

View File

@@ -69,6 +69,12 @@ import {
guideModelLoadManager,
type GuideModelResourceSet
} from '@/services/model/GuideModelLoadManager'
import {
GuideAutoSwitchStateMachine,
type GuideAutoSwitchDirection,
type GuideAutoSwitchInputSource,
type GuideAutoSwitchRequest
} from './guideAutoSwitchStateMachine'
type ViewMode = 'overview' | 'floor' | 'multi'
type CameraPreset = 'top' | 'oblique'
@@ -193,6 +199,7 @@ interface CameraFitOptions {
targetOffsetRatio?: THREE.Vector3
screenOffsetRatio?: THREE.Vector2
up?: THREE.Vector3
onComplete?: () => void
}
interface LoadFloorOptions {
@@ -337,8 +344,6 @@ const props = withDefaults(defineProps<{
showRoute?: boolean
autoSwitch?: boolean
disableAutoExit?: boolean
autoSwitchThresholdLow?: number
autoSwitchThresholdHigh?: number
autoSwitchCooldown?: number
}>(), {
assetBaseUrl: '',
@@ -354,9 +359,7 @@ const props = withDefaults(defineProps<{
showRoute: false,
autoSwitch: true,
disableAutoExit: false,
autoSwitchThresholdLow: 1.45,
autoSwitchThresholdHigh: 3.8,
autoSwitchCooldown: 2000
autoSwitchCooldown: 1500
})
const emit = defineEmits<{
@@ -456,28 +459,14 @@ let desktopRotateModifiers = {
}
let cachedOverviewModel: THREE.Object3D | null = null
let cachedSharedModelUrl = ''
let autoSwitchOverviewMaxDim = 0
// 自动切换状态:建筑外观积极进入,馆内锁定后再二段退出
// 自动切换状态:所有缩放输入统一使用相对距离状态机
let isAutoSwitchLocked = false
let lastAutoSwitchTime = 0
let lastControlDistance = 0
let activeControlGestureStartedAt = 0
let indoorEntryGestureLocked = false
let indoorContextLockedUntil = 0
let floorExitCandidateStartedAt = 0
let floorExitCandidateBaseDistance = 0
let floorExitZoomOutAccumulatedRatio = 0
let autoSwitchTemporarilyDisabled = false
let autoSwitchDisableTimer: ReturnType<typeof setTimeout> | null = null
let overviewEntryIntentStartedAt = 0
let modelAdjustReportTimer: ReturnType<typeof setTimeout> | null = null
let hasPendingManualModelAdjustment = false
let overviewEntryStartDistance = 0
let overviewEntryZoomInRounds = 0
let overviewWheelZoomInRounds = 0
let overviewWheelZoomIntentUntil = 0
let overviewWheelAutoSwitchTimer: ReturnType<typeof setTimeout> | null = null
let activeAutoSwitchInputSource: GuideAutoSwitchInputSource = 'gesture'
let isProgrammaticCameraChange = false
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
let cameraTween: CameraTweenState | null = null
@@ -495,21 +484,9 @@ const poiScreenHitRadiusPx = 64
const poiScreenHitRadiusCorePx = 76
const poiAmbientLabelHallSpacingPx = 92
const poiAmbientLabelServiceSpacingPx = 72
const indoorContextLockMs = 5000
const indoorGestureExitGraceMs = 900
const overviewEntryIntentWindowMs = 1200
const overviewEntryMinZoomInRatio = 0.04
const overviewEntryAccumulatedRatio = 0.2
const overviewEntryMinRounds = 2
const overviewEntryMaxRounds = 4
const overviewWheelEntryDelayMs = 180
const floorExitCandidateThresholdMultiplier = 1.12
const floorExitConfirmThresholdMultiplier = 1.34
const floorExitZoomOutIntentMinDeltaRatio = 0.015
const floorExitAccumulatedZoomOutRatio = 0.26
const floorExitCandidateHoldMs = 900
const modelLoadRetryDelaysMs = [300, 900]
const adjacentPreloadDelayMs = 900
const manualAutoSwitchPauseMs = 1500
const poiMarkerVisualLiftFactor = 0.92
const poiAmbientLabelLiftFactor = 2.1
const poiFocusLabelLiftFactor = 2.72
@@ -1593,7 +1570,10 @@ const cancelCameraTween = (options: { manual?: boolean } = {}) => {
clearProgrammaticCameraTimer()
const distance = controls?.getDistance()
if (typeof distance === 'number' && Number.isFinite(distance)) {
lastControlDistance = distance
if (activeView.value === 'floor') {
// 用户中断楼层初始拟合时,使用中断后的稳定距离重新建立退出基准。
autoSwitchStateMachine.setFloorInitialDistance(distance)
}
}
}
}
@@ -1610,11 +1590,6 @@ const moveCameraTo = (
isProgrammaticCameraChange = true
clearProgrammaticCameraTimer()
const nextDistance = toPosition.distanceTo(toTarget)
if (Number.isFinite(nextDistance)) {
lastControlDistance = nextDistance
}
if (!cameraTweenEnabled || options.immediate) {
cameraTween = null
camera.position.copy(toPosition)
@@ -1689,7 +1664,10 @@ const updatePoiTapFeedback = (now: number) => {
}
const handleControlStart = () => {
activeControlGestureStartedAt = Date.now()
const distance = controls?.getDistance()
if (typeof distance === 'number' && Number.isFinite(distance)) {
autoSwitchStateMachine.beginInput(distance, activeAutoSwitchInputSource)
}
if (!isProgrammaticCameraChange) {
hasPendingManualModelAdjustment = true
clearModelAdjustReportTimer()
@@ -1698,12 +1676,7 @@ const handleControlStart = () => {
}
const handleControlEnd = () => {
if (indoorEntryGestureLocked) {
indoorContextLockedUntil = Math.max(indoorContextLockedUntil, Date.now() + indoorGestureExitGraceMs)
indoorEntryGestureLocked = false
}
activeControlGestureStartedAt = 0
updateLastControlDistance()
activeAutoSwitchInputSource = 'gesture'
scheduleModelAdjustIdleReport()
}
@@ -2491,7 +2464,6 @@ const showFocusHallHighlight = (poi: RenderPoi) => {
}
const disposeCachedOverviewModel = () => {
autoSwitchOverviewMaxDim = 0
if (!cachedOverviewModel) return
disposeObject(cachedOverviewModel)
@@ -2885,15 +2857,6 @@ const getObjectSize = (object: THREE.Object3D) => (
getObjectBox(object).getSize(new THREE.Vector3())
)
const getObjectMaxDim = (object: THREE.Object3D) => {
const size = getObjectSize(object)
return Math.max(size.x, size.y, size.z, 1)
}
const cacheAutoSwitchOverviewMaxDim = (model: THREE.Object3D) => {
autoSwitchOverviewMaxDim = getObjectMaxDim(model)
}
const getMultiFloorVerticalGap = (items: MultiFloorModelItem[]) => {
const maxFloorHeight = Math.max(...items.map((item) => item.size.y), 1)
const maxFootprint = Math.max(...items.map((item) => Math.max(item.size.x, item.size.z)), 1)
@@ -3096,229 +3059,58 @@ const disposeDetachedMultiFloorModels = (
.forEach((item) => disposeObject(item.model, protectedResources))
}
const updateLastControlDistance = () => {
if (!controls) return
const distance = controls.getDistance()
if (Number.isFinite(distance)) {
lastControlDistance = distance
}
}
const resetOverviewEntryIntent = () => {
overviewEntryIntentStartedAt = 0
overviewEntryStartDistance = 0
overviewEntryZoomInRounds = 0
overviewWheelZoomInRounds = 0
overviewWheelZoomIntentUntil = 0
}
const resetFloorExitCandidate = () => {
floorExitCandidateStartedAt = 0
floorExitCandidateBaseDistance = 0
floorExitZoomOutAccumulatedRatio = 0
}
const markFloorAutoSwitchEntry = () => {
indoorContextLockedUntil = Date.now() + indoorContextLockMs
indoorEntryGestureLocked = activeControlGestureStartedAt > 0
resetOverviewEntryIntent()
resetFloorExitCandidate()
updateLastControlDistance()
}
const resetAutoSwitchDistanceTracking = () => {
activeControlGestureStartedAt = 0
indoorEntryGestureLocked = false
indoorContextLockedUntil = 0
resetOverviewEntryIntent()
resetFloorExitCandidate()
updateLastControlDistance()
activeAutoSwitchInputSource = 'gesture'
autoSwitchStateMachine.reset(activeView.value)
}
const clearOverviewWheelAutoSwitchTimer = () => {
if (overviewWheelAutoSwitchTimer) {
clearTimeout(overviewWheelAutoSwitchTimer)
overviewWheelAutoSwitchTimer = null
}
}
const hasOverviewWheelEntryRounds = () => (
activeView.value === 'overview'
&& overviewWheelZoomInRounds >= overviewEntryMinRounds
)
const hasPendingOverviewWheelEntryIntent = (now: number) => (
hasOverviewWheelEntryRounds()
&& isOverviewEntryIntentActive(now)
)
const canRunPendingOverviewEntry = (now: number) => (
props.autoSwitch
&& !autoSwitchTemporarilyDisabled
&& !isLoading.value
&& !isAutoSwitchLocked
&& now - lastAutoSwitchTime >= props.autoSwitchCooldown
&& Boolean(controls && activeModel)
)
const handleWheelIntent = (event: WheelEvent) => {
if (!controls || activeView.value !== 'overview' || event.deltaY >= 0) return
if (!controls || activeView.value !== 'overview' || event.deltaY === 0) return
const distance = controls.getDistance()
if (!Number.isFinite(distance)) return
const now = Date.now()
if (!isOverviewEntryIntentActive(now)) {
overviewEntryIntentStartedAt = now
overviewEntryStartDistance = distance
overviewEntryZoomInRounds = 0
overviewWheelZoomInRounds = 0
}
overviewEntryIntentStartedAt = now
overviewEntryZoomInRounds += 1
overviewWheelZoomInRounds += 1
overviewWheelZoomIntentUntil = now + 900
clearOverviewWheelAutoSwitchTimer()
overviewWheelAutoSwitchTimer = setTimeout(() => {
const triggerNow = Date.now()
overviewWheelAutoSwitchTimer = null
if (hasPendingOverviewWheelEntryIntent(triggerNow) && canRunPendingOverviewEntry(triggerNow)) {
checkAutoSwitch({ forceOverviewEntry: true })
}
}, overviewWheelEntryDelayMs)
activeAutoSwitchInputSource = 'wheel'
autoSwitchStateMachine.beginInput(distance, 'wheel')
}
const isOverviewEntryIntentActive = (now: number) => (
overviewEntryIntentStartedAt > 0
&& (
now - overviewEntryIntentStartedAt <= overviewEntryIntentWindowMs
|| now <= overviewWheelZoomIntentUntil
)
)
const getOverviewEntryAccumulatedRatio = (distance: number) => {
if (!overviewEntryStartDistance || !Number.isFinite(distance)) return 0
return Math.max(0, 1 - distance / overviewEntryStartDistance)
}
const trackOverviewEntryIntent = (distance: number, previousDistance: number, now: number) => {
if (!Number.isFinite(distance) || !Number.isFinite(previousDistance) || previousDistance <= 0) return
const zoomInRatio = (previousDistance - distance) / previousDistance
const hasClearZoomIn = zoomInRatio >= overviewEntryMinZoomInRatio
if (!hasClearZoomIn) {
if (!isOverviewEntryIntentActive(now)) {
resetOverviewEntryIntent()
}
return
}
if (!isOverviewEntryIntentActive(now) || !overviewEntryStartDistance || distance > overviewEntryStartDistance) {
overviewEntryIntentStartedAt = now
overviewEntryStartDistance = previousDistance
overviewEntryZoomInRounds = 0
}
overviewEntryIntentStartedAt = now
overviewEntryZoomInRounds += 1
const accumulatedRatio = getOverviewEntryAccumulatedRatio(distance)
overviewEntryZoomInRounds = Math.max(
overviewEntryZoomInRounds,
Math.floor(accumulatedRatio / overviewEntryMinZoomInRatio)
)
}
const shouldEnterFloorFromOverview = (
distance: number,
maxDim: number,
now: number,
hasForcedOverviewEntryIntent = false
) => {
const enterFloorAt = maxDim * props.autoSwitchThresholdLow
if (distance <= enterFloorAt) return true
if (hasForcedOverviewEntryIntent) return true
if (!isOverviewEntryIntentActive(now)) return false
const accumulatedRatio = getOverviewEntryAccumulatedRatio(distance)
const hasRepeatedZoomIn = overviewEntryZoomInRounds >= overviewEntryMinRounds
&& accumulatedRatio >= overviewEntryAccumulatedRatio
const hasSustainedZoomIn = overviewEntryZoomInRounds >= overviewEntryMaxRounds
&& accumulatedRatio >= overviewEntryAccumulatedRatio * 0.68
return hasRepeatedZoomIn || hasSustainedZoomIn
}
const isFloorAutoExitLocked = (now: number) => (
props.disableAutoExit
|| Boolean(props.targetFocus)
|| indoorEntryGestureLocked
|| now < indoorContextLockedUntil
)
const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
const now = Date.now()
const hasForcedOverviewEntryIntent = options.forceOverviewEntry === true
&& hasOverviewWheelEntryRounds()
&& canRunPendingOverviewEntry(now)
const canRunAutoSwitch = (direction: GuideAutoSwitchDirection) => {
if (
!props.autoSwitch
|| autoSwitchTemporarilyDisabled
|| isLoading.value
|| (!hasForcedOverviewEntryIntent && isProgrammaticCameraChange)
|| isAutoSwitchLocked
|| isProgrammaticCameraChange
|| !controls
|| !activeModel
|| activeView.value === 'multi'
) {
return false
}
if (direction === 'exit-overview') {
return !props.disableAutoExit && !props.targetFocus && activeView.value === 'floor'
}
return activeView.value === 'overview'
}
const requestAutoSwitch = (request: GuideAutoSwitchRequest) => {
if (!canRunAutoSwitch(request.direction)) {
autoSwitchStateMachine.markTransitionFailed()
return
}
if (isAutoSwitchLocked) {
emit('autoSwitchBlocked', { reason: 'loading-locked' })
return
}
if (now - lastAutoSwitchTime < props.autoSwitchCooldown) {
emit('autoSwitchBlocked', { reason: 'cooldown' })
return
}
if (!controls || !activeModel || activeView.value === 'multi') return
const distance = controls.getDistance()
const previousDistance = lastControlDistance || distance
const distanceDelta = distance - previousDistance
lastControlDistance = distance
// 自动切换使用全馆稳定尺寸,避免单楼层可见性过滤压低 floor -> overview 阈值。
const maxDim = autoSwitchOverviewMaxDim || getObjectMaxDim(activeModel)
if (activeView.value === 'overview') {
if (!hasForcedOverviewEntryIntent) {
trackOverviewEntryIntent(distance, previousDistance, now)
}
if (!shouldEnterFloorFromOverview(distance, maxDim, now, hasForcedOverviewEntryIntent)) {
return
}
isAutoSwitchLocked = true
if (request.direction === 'enter-floor') {
const targetFloorId = getAutoEntryTargetFloorId()
const hasSceneToKeep = hasRenderableSceneForFloorTransition()
isAutoSwitchLocked = true
lastAutoSwitchTime = now
resetFloorExitCandidate()
resetOverviewEntryIntent()
clearOverviewWheelAutoSwitchTimer()
void runAutoSwitchLoad(
{
from: 'overview',
to: 'floor',
trigger: 'zoom-in',
distance
distance: request.distance
},
async () => {
await loadFloor(targetFloorId, {
@@ -3332,65 +3124,33 @@ const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
return
}
if (activeView.value !== 'floor') {
resetFloorExitCandidate()
return
}
if (isFloorAutoExitLocked(now)) {
resetFloorExitCandidate()
return
}
const baseExitThreshold = maxDim * props.autoSwitchThresholdHigh
const candidateExitAt = baseExitThreshold * floorExitCandidateThresholdMultiplier
const confirmExitAt = baseExitThreshold * floorExitConfirmThresholdMultiplier
const isBeyondCandidateThreshold = distance >= candidateExitAt
if (!isBeyondCandidateThreshold) {
resetFloorExitCandidate()
return
}
if (!floorExitCandidateStartedAt) {
floorExitCandidateStartedAt = now
floorExitCandidateBaseDistance = Math.max(previousDistance, distance, 1)
floorExitZoomOutAccumulatedRatio = 0
return
}
const zoomOutRatio = previousDistance > 0 ? distanceDelta / previousDistance : 0
const isClearZoomOutIntent = zoomOutRatio >= floorExitZoomOutIntentMinDeltaRatio
if (isClearZoomOutIntent || distance > floorExitCandidateBaseDistance) {
floorExitZoomOutAccumulatedRatio = Math.max(
floorExitZoomOutAccumulatedRatio,
distance / Math.max(floorExitCandidateBaseDistance, 1) - 1
)
}
const hasConfirmedScale = distance >= confirmExitAt
&& floorExitZoomOutAccumulatedRatio >= floorExitAccumulatedZoomOutRatio
const hasHeldCandidate = now - floorExitCandidateStartedAt >= floorExitCandidateHoldMs
if (!hasConfirmedScale || !hasHeldCandidate) {
return
}
isAutoSwitchLocked = true
lastAutoSwitchTime = now
resetFloorExitCandidate()
void runAutoSwitchLoad(
{
from: 'floor',
to: 'overview',
trigger: 'zoom-out',
distance
distance: request.distance
},
loadOverview
)
}
const autoSwitchStateMachine = new GuideAutoSwitchStateMachine({
cooldownMs: props.autoSwitchCooldown,
canSwitch: canRunAutoSwitch,
onSwitchRequested: requestAutoSwitch
})
const checkAutoSwitch = (source: GuideAutoSwitchInputSource = activeAutoSwitchInputSource) => {
if (!controls || isProgrammaticCameraChange) return
const distance = controls.getDistance()
if (!Number.isFinite(distance)) return
autoSwitchStateMachine.setView(activeView.value)
autoSwitchStateMachine.updateDistance(distance, { source })
}
const runAutoSwitchLoad = async (
event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: 'zoom-in' | 'zoom-out'; distance: number },
loadTask: () => Promise<unknown>,
@@ -3407,8 +3167,10 @@ const runAutoSwitchLoad = async (
if (showLoading) {
isLoading.value = false
}
autoSwitchStateMachine.markTransitionSucceeded()
emit('autoSwitch', event)
} catch (error) {
autoSwitchStateMachine.markTransitionFailed()
if (isStaleModelLoadError(error)) return
console.error('馆内 3D 自动视角切换失败:', error)
@@ -3475,7 +3237,7 @@ const setCameraView = (
}
controls.minDistance = Math.max(SGS_VISUAL_RENDER_CONFIG.controls.minDistance, maxDim * 0.08)
controls.maxDistance = Math.max(SGS_VISUAL_RENDER_CONFIG.controls.maxDistance, maxDim * 8)
moveCameraTo(nextPosition, nextTarget)
moveCameraTo(nextPosition, nextTarget, { onComplete: options.onComplete })
}
const fitCameraToObject = (object: THREE.Object3D, preset: CameraPreset = 'oblique') => {
@@ -3503,8 +3265,17 @@ const fitCameraToObject = (object: THREE.Object3D, preset: CameraPreset = 'obliq
screenOffsetRatio: SGS_VISUAL_RENDER_CONFIG.framing.floorScreenOffsetRatio,
up: new THREE.Vector3(0, 1, 0)
}
: {}
: {}
overviewFitOptions.onComplete = () => {
if (activeView.value !== 'floor' || !controls) return
const distance = controls.getDistance()
if (Number.isFinite(distance)) {
// 楼层切换或相机复位完成后,以最终拟合距离作为退出基准。
autoSwitchStateMachine.setFloorInitialDistance(distance)
}
}
setCameraView(center, maxDim, direction, overviewFitOptions)
}
@@ -3523,11 +3294,6 @@ const applyOverviewInitialCamera = () => {
camera.updateProjectionMatrix()
controls.target.copy(OVERVIEW_INITIAL_CAMERA_PARAMS.target)
controls.update()
const distance = camera.position.distanceTo(controls.target)
if (Number.isFinite(distance)) {
lastControlDistance = distance
}
}
const resetCamera = () => {
@@ -3555,33 +3321,16 @@ const zoomCamera = (direction: 'in' | 'out', options: { source?: ZoomCameraSourc
if (!Number.isFinite(nextDistance) || nextDistance <= 0) return
const shouldTrackUserZoom = options.source === 'button'
if (shouldTrackUserZoom && direction === 'in' && activeView.value === 'overview') {
const now = Date.now()
if (!isOverviewEntryIntentActive(now)) {
overviewEntryIntentStartedAt = now
overviewEntryStartDistance = currentDistance
overviewEntryZoomInRounds = 0
}
overviewEntryIntentStartedAt = now
overviewEntryZoomInRounds += 1
overviewWheelZoomInRounds += 1
overviewWheelZoomIntentUntil = now + 900
if (shouldTrackUserZoom) {
activeAutoSwitchInputSource = 'button'
autoSwitchStateMachine.beginInput(currentDistance, 'button')
}
offset.setLength(nextDistance)
const nextPosition = controls.target.clone().add(offset)
moveCameraTo(nextPosition, controls.target, {
onComplete: handleControlChange
onComplete: () => handleControlChange()
})
if (shouldTrackUserZoom) {
lastControlDistance = currentDistance
checkAutoSwitch({
forceOverviewEntry: direction === 'in' && activeView.value === 'overview'
})
}
handleControlChange()
}
const loadCurrentFloorPoiMarkers = async (loadToken: number) => {
@@ -3635,6 +3384,7 @@ const loadOverview = async () => {
const loadToken = startModelLoad()
activeView.value = 'overview'
autoSwitchStateMachine.setView('overview')
syncControlInteractionOptions()
clearSceneData()
@@ -3647,7 +3397,6 @@ const loadOverview = async () => {
activeModel.visible = true
applyIndoorInitialModelTransform(activeModel)
applyModelVisibilityForView(activeModel, 'overview')
cacheAutoSwitchOverviewMaxDim(activeModel)
targetScene.add(activeModel)
applyOverviewInitialCamera()
loadCurrentFloorPoiMarkersInBackground(loadToken)
@@ -3677,7 +3426,6 @@ const loadOverview = async () => {
prepareModel(activeModel)
applyIndoorInitialModelTransform(activeModel)
applyModelVisibilityForView(activeModel, 'overview')
cacheAutoSwitchOverviewMaxDim(activeModel)
cachedOverviewModel = activeModel
cachedSharedModelUrl = packageData.overviewModelUrl
targetScene.add(activeModel)
@@ -3742,10 +3490,6 @@ const prepareFloorScene = async (
applyIndoorInitialModelTransform(model)
applyModelVisibilityForView(model, 'floor', requestedFloorId)
cacheAsSharedModel = Boolean(floor.sharedModelAsset)
if (cacheAsSharedModel) {
cacheAutoSwitchOverviewMaxDim(model)
}
}
if (!model) {
@@ -3786,6 +3530,8 @@ const commitPreparedFloorScene = (
}
activeView.value = 'floor'
autoSwitchStateMachine.setView('floor')
autoSwitchStateMachine.clearFloorInitialDistance()
syncControlInteractionOptions()
currentFloor.value = expectedFloorId
clearSceneData()
@@ -3812,7 +3558,6 @@ const commitPreparedFloorScene = (
updatePoiMarkerFocus()
hasLoadedFloorViewOnce = true
fitCameraToObject(activeModel)
markFloorAutoSwitchEntry()
refreshPoiVisibilityByDistance()
renderRoutePreview()
markFirstModelVisible('floor', {
@@ -3869,6 +3614,7 @@ const loadMultiFloor = async () => {
const loadToken = startModelLoad()
activeView.value = 'multi'
autoSwitchStateMachine.setView('multi')
syncControlInteractionOptions()
clearSceneData()
setProgress(18, '正在加载多层展示模型...')
@@ -3897,7 +3643,6 @@ const loadMultiFloor = async () => {
sourceModel.name = 'GuideOverviewModel'
prepareModel(sourceModel)
applyModelVisibilityForView(sourceModel, 'overview')
cacheAutoSwitchOverviewMaxDim(sourceModel)
cachedOverviewModel = sourceModel
cachedSharedModelUrl = sharedModelUrl
}
@@ -4791,7 +4536,7 @@ const handleSceneTap = async (event: PointerEvent) => {
if (hitMarker?.userData.poi) {
const selectedPoi = hitMarker.userData.poi as RenderPoi
if (activeView.value === 'overview') {
disableAutoSwitchTemporarily(10000)
disableAutoSwitchTemporarily(manualAutoSwitchPauseMs)
try {
await loadFloor(selectedPoi.floorId, {
preserveCurrentSceneUntilReady: hasRenderableSceneForFloorTransition(),
@@ -4826,7 +4571,7 @@ const handleSceneTap = async (event: PointerEvent) => {
.find((object): object is THREE.Object3D => Boolean(object))
if (floorObject?.userData.floorId) {
disableAutoSwitchTemporarily(10000)
disableAutoSwitchTemporarily(manualAutoSwitchPauseMs)
void handleFloorChange(floorObject.userData.floorId as string)
return
}
@@ -4841,6 +4586,12 @@ const handlePointerDown = (event: PointerEvent) => {
if (event.pointerType === 'touch' && getActiveTouchPointerCount() >= 2) {
hadMultiPointerGesture = true
pointerDownState = null
activeAutoSwitchInputSource = 'touch'
const distance = controls?.getDistance()
if (typeof distance === 'number' && Number.isFinite(distance)) {
// 双指手势从第二个触点按下时重新记录连续缩放基准。
autoSwitchStateMachine.beginInput(distance, 'touch', { resetBaseline: true })
}
}
if (event.pointerType !== 'touch') {
updateDesktopRotateModifierState(event)
@@ -5273,19 +5024,14 @@ const disposeScene = () => {
autoSwitchDisableTimer = null
}
clearOverviewWheelAutoSwitchTimer()
autoSwitchStateMachine.dispose()
clearProgrammaticCameraTimer()
clearModelAdjustReportTimer()
hasPendingManualModelAdjustment = false
cameraTween = null
isProgrammaticCameraChange = false
lastControlDistance = 0
activeControlGestureStartedAt = 0
activeAutoSwitchInputSource = 'gesture'
resetInteractionGateState()
indoorEntryGestureLocked = false
indoorContextLockedUntil = 0
resetOverviewEntryIntent()
resetFloorExitCandidate()
clearSceneData()
disposePoiMarkerCache()