@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user