修复三维楼层自动切换与模型加载

- 优化建筑外观与馆内楼层的缩放自动切换状态
- 首次进入馆内默认加载 1F 楼层模型,后续重进保持上次楼层
- 手动楼层切换强制加载对应楼层模型,避免误用外观兜底
- 调整三维渲染色调、背景和灯光,减轻楼层模型灰蒙感

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lyf
2026-06-29 20:58:26 +08:00
parent 4c9d480cce
commit 93bd1ddab3

View File

@@ -83,6 +83,7 @@ interface CameraFitOptions {
interface LoadFloorOptions {
preserveCurrentSceneUntilReady?: boolean
suppressProgress?: boolean
allowOverviewFallback?: boolean
}
interface LoadModelOptions {
@@ -190,6 +191,7 @@ const props = withDefaults(defineProps<{
routePreview?: GuideRouteResult | null
showRoute?: boolean
autoSwitch?: boolean
disableAutoExit?: boolean
autoSwitchThresholdLow?: number
autoSwitchThresholdHigh?: number
autoSwitchCooldown?: number
@@ -205,6 +207,7 @@ const props = withDefaults(defineProps<{
routePreview: null,
showRoute: false,
autoSwitch: true,
disableAutoExit: false,
autoSwitchThresholdLow: 1.45,
autoSwitchThresholdHigh: 3.8,
autoSwitchCooldown: 2000
@@ -223,7 +226,7 @@ const containerRef = ref<ContainerRef>(null)
const isLoading = ref(true)
const loadError = ref(false)
const loadingProgress = ref(0)
const loadingMessage = ref('正在读取内导览资源...')
const loadingMessage = ref('正在读取内导览资源...')
const activeView = ref<ViewMode>(props.initialView)
const currentFloor = ref(props.initialFloorId)
const selectedPOI = ref<RenderPoi | null>(null)
@@ -264,22 +267,28 @@ let isDisposed = false
let pendingTargetFocus: TargetPoiFocusRequest | null = null
let targetFocusQueue: Promise<unknown> = Promise.resolve()
let modelLoadVersion = 0
let hasLoadedFloorViewOnce = false
let pointerDownState: { x: number; y: number } | null = null
let cachedOverviewModel: THREE.Object3D | null = null
let cachedSharedModelUrl = ''
let autoSwitchOverviewMaxDim = 0
// 自动切换状态
// 自动切换状态:建筑外观积极进入,馆内锁定后再二段退出。
let isAutoSwitchLocked = false
let lastAutoSwitchTime = 0
let lastControlDistance = 0
let floorAutoSwitchProtectedUntil = 0
let floorExitThresholdExceededSince = 0
let floorExitZoomOutAccumulatedDistance = 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 suppressNextAutoSwitchCheck = false
let overviewWheelZoomStartDistance = 0
let overviewEntryIntentStartedAt = 0
let overviewEntryStartDistance = 0
let overviewEntryZoomInRounds = 0
let overviewWheelZoomInRounds = 0
let overviewWheelZoomIntentUntil = 0
let overviewWheelAutoSwitchTimer: ReturnType<typeof setTimeout> | null = null
let isProgrammaticCameraChange = false
@@ -293,10 +302,19 @@ const poiHitTargetScaleMultiplier = 4.8
const poiHitTargetCoreScaleMultiplier = 5.4
const poiScreenHitRadiusPx = 64
const poiScreenHitRadiusCorePx = 76
const floorAutoSwitchProtectionMs = 1600
const floorExitZoomOutIntentMinDeltaRatio = 0.01
const floorExitZoomOutAccumulatedRatio = 0.12
const floorExitThresholdHoldMs = 650
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 syncControlInteractionOptions = () => {
if (!controls) return
@@ -359,7 +377,7 @@ const floorExteriorNameKeywords = [
'玻璃幕墙',
'楼顶',
'屋顶',
'外'
'外'
]
const overviewBuildingSubjectNameKeywords = [
@@ -727,11 +745,6 @@ const refreshPoiVisibilityByDistance = () => {
const handleControlChange = () => {
updatePoiVisibilityByDistance()
if (suppressNextAutoSwitchCheck) {
suppressNextAutoSwitchCheck = false
updateLastControlDistance()
return
}
checkAutoSwitch()
}
@@ -862,9 +875,19 @@ const updatePoiTapFeedback = (now: number) => {
}
const handleControlStart = () => {
activeControlGestureStartedAt = Date.now()
cancelCameraTween({ manual: true })
}
const handleControlEnd = () => {
if (indoorEntryGestureLocked) {
indoorContextLockedUntil = Math.max(indoorContextLockedUntil, Date.now() + indoorGestureExitGraceMs)
indoorEntryGestureLocked = false
}
activeControlGestureStartedAt = 0
updateLastControlDistance()
}
const setProgress = (progress: number, message: string) => {
loadingProgress.value = Math.min(100, Math.max(0, progress))
loadingMessage.value = message
@@ -923,7 +946,7 @@ const initThree = async () => {
}
scene = new THREE.Scene()
scene.background = new THREE.Color(0xf1f3ee)
scene.background = new THREE.Color(0xf7f8f4)
const width = Math.max(1, container.clientWidth)
const height = Math.max(1, container.clientHeight)
@@ -938,8 +961,8 @@ const initThree = async () => {
renderer.setSize(width, height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
renderer.outputColorSpace = THREE.SRGBColorSpace
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 1.05
renderer.toneMapping = THREE.NeutralToneMapping
renderer.toneMappingExposure = 1.0
container.appendChild(renderer.domElement)
controls = new OrbitControls(camera, renderer.domElement)
@@ -964,14 +987,14 @@ const initThree = async () => {
routeGroup.name = 'GuideModelRoute'
scene.add(routeGroup)
const ambientLight = new THREE.AmbientLight(0xffffff, 1.6)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.9)
scene.add(ambientLight)
const keyLight = new THREE.DirectionalLight(0xffffff, 2.2)
const keyLight = new THREE.DirectionalLight(0xffffff, 2.0)
keyLight.position.set(80, 120, 60)
scene.add(keyLight)
const fillLight = new THREE.DirectionalLight(0xdfeaff, 0.9)
const fillLight = new THREE.DirectionalLight(0xdfeaff, 0.45)
fillLight.position.set(-80, 60, -80)
scene.add(fillLight)
@@ -979,11 +1002,12 @@ const initThree = async () => {
resizeObserver.observe(container)
container.addEventListener('pointerdown', handlePointerDown)
container.addEventListener('pointerup', handlePointerUp)
container.addEventListener('wheel', handleWheelIntent, { passive: true })
renderer.domElement.addEventListener('wheel', handleWheelIntent, { passive: true, capture: true })
// 监听控制器变化,用于自动切换和点位可见层级更新
if (controls) {
controls.addEventListener('start', handleControlStart)
controls.addEventListener('end', handleControlEnd)
controls.addEventListener('change', handleControlChange)
}
@@ -1743,11 +1767,27 @@ const canAttachCachedSharedModel = (modelUrl: string) => (
Boolean(cachedOverviewModel && cachedSharedModelUrl === modelUrl && scene)
)
const canAttachCachedOverviewModel = () => Boolean(cachedOverviewModel && scene)
const canLoadFloorSilently = (floorId: string) => {
const floor = floorIndex.value.find((item) => item.floorId === floorId)
return Boolean(floor && canAttachCachedSharedModel(floor.modelUrl))
}
const getDefaultFloorId = () => (
floorIndex.value.find((floor) => floor.floorId === props.initialFloorId)?.floorId
|| floorIndex.value.find((floor) => floor.floorId === 'L1')?.floorId
|| floorIndex.value[0]?.floorId
|| props.initialFloorId
|| 'L1'
)
const getAutoEntryTargetFloorId = () => (
hasLoadedFloorViewOnce
? currentFloor.value || getDefaultFloorId()
: getDefaultFloorId()
)
const hasRenderableSceneForFloorTransition = () => Boolean(scene && activeModel)
const createMultiFloorItemFromSharedModel = (sourceModel: THREE.Object3D, floor: FloorIndexItem) => {
@@ -1784,19 +1824,34 @@ const updateLastControlDistance = () => {
}
}
const resetOverviewEntryIntent = () => {
overviewEntryIntentStartedAt = 0
overviewEntryStartDistance = 0
overviewEntryZoomInRounds = 0
overviewWheelZoomInRounds = 0
overviewWheelZoomIntentUntil = 0
}
const resetFloorExitCandidate = () => {
floorExitCandidateStartedAt = 0
floorExitCandidateBaseDistance = 0
floorExitZoomOutAccumulatedRatio = 0
}
const markFloorAutoSwitchEntry = () => {
floorAutoSwitchProtectedUntil = Date.now() + floorAutoSwitchProtectionMs
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
indoorContextLockedUntil = Date.now() + indoorContextLockMs
indoorEntryGestureLocked = activeControlGestureStartedAt > 0
resetOverviewEntryIntent()
resetFloorExitCandidate()
updateLastControlDistance()
}
const resetAutoSwitchDistanceTracking = () => {
floorAutoSwitchProtectedUntil = 0
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
overviewWheelZoomStartDistance = 0
overviewWheelZoomIntentUntil = 0
activeControlGestureStartedAt = 0
indoorEntryGestureLocked = false
indoorContextLockedUntil = 0
resetOverviewEntryIntent()
resetFloorExitCandidate()
updateLastControlDistance()
}
@@ -1807,6 +1862,25 @@ const clearOverviewWheelAutoSwitchTimer = () => {
}
}
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
@@ -1814,47 +1888,125 @@ const handleWheelIntent = (event: WheelEvent) => {
if (!Number.isFinite(distance)) return
const now = Date.now()
if (!overviewWheelZoomStartDistance || now > overviewWheelZoomIntentUntil) {
overviewWheelZoomStartDistance = distance
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
checkAutoSwitch({ forceOverviewEntry: true })
}, 180)
if (hasPendingOverviewWheelEntryIntent(triggerNow) && canRunPendingOverviewEntry(triggerNow)) {
checkAutoSwitch({ forceOverviewEntry: true })
}
}, overviewWheelEntryDelayMs)
}
const shouldForceOverviewEntryFromWheel = (distance: number, now: number) => (
now <= overviewWheelZoomIntentUntil
&& overviewWheelZoomStartDistance > 0
&& distance <= overviewWheelZoomStartDistance * 0.86
const isOverviewEntryIntentActive = (now: number) => (
overviewEntryIntentStartedAt > 0
&& (
now - overviewEntryIntentStartedAt <= overviewEntryIntentWindowMs
|| now <= overviewWheelZoomIntentUntil
)
)
const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
// 检查是否启用自动切换
if (!props.autoSwitch || autoSwitchTemporarilyDisabled || isProgrammaticCameraChange || isLoading.value) {
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)
if (
!props.autoSwitch
|| autoSwitchTemporarilyDisabled
|| isLoading.value
|| (!hasForcedOverviewEntryIntent && isProgrammaticCameraChange)
) {
return
}
// 检查加载锁
if (isAutoSwitchLocked) {
emit('autoSwitchBlocked', { reason: 'loading-locked' })
return
}
// 检查冷却时间
const now = Date.now()
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
@@ -1863,20 +2015,21 @@ const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
// 自动切换使用全馆稳定尺寸,避免单楼层可见性过滤压低 floor -> overview 阈值。
const maxDim = autoSwitchOverviewMaxDim || getObjectMaxDim(activeModel)
const enterFloorAt = maxDim * props.autoSwitchThresholdLow
const exitFloorAt = maxDim * props.autoSwitchThresholdHigh
if (activeView.value === 'overview') {
if (!hasForcedOverviewEntryIntent) {
trackOverviewEntryIntent(distance, previousDistance, now)
}
// 判断是否需要切换
if (activeView.value === 'overview' && (distance < enterFloorAt || (options.forceOverviewEntry && shouldForceOverviewEntryFromWheel(distance, now)))) {
// 从建筑外观自动切换到单楼层
const targetFloorId = currentFloor.value || props.initialFloorId || 'L1'
if (!shouldEnterFloorFromOverview(distance, maxDim, now, hasForcedOverviewEntryIntent)) {
return
}
const targetFloorId = getAutoEntryTargetFloorId()
const hasSceneToKeep = hasRenderableSceneForFloorTransition()
isAutoSwitchLocked = true
lastAutoSwitchTime = now
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
overviewWheelZoomStartDistance = 0
overviewWheelZoomIntentUntil = 0
resetFloorExitCandidate()
resetOverviewEntryIntent()
clearOverviewWheelAutoSwitchTimer()
void runAutoSwitchLoad(
@@ -1888,7 +2041,8 @@ const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
},
() => loadFloor(targetFloorId, {
preserveCurrentSceneUntilReady: hasSceneToKeep,
suppressProgress: hasSceneToKeep
suppressProgress: hasSceneToKeep,
allowOverviewFallback: true
}),
'单楼层模型加载失败',
{ showLoading: !hasSceneToKeep && !canLoadFloorSilently(targetFloorId) }
@@ -1896,61 +2050,64 @@ const checkAutoSwitch = (options: { forceOverviewEntry?: boolean } = {}) => {
return
}
if (activeView.value !== 'floor' || props.targetFocus) {
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
if (activeView.value !== 'floor') {
resetFloorExitCandidate()
return
}
if (now < floorAutoSwitchProtectedUntil) {
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
if (isFloorAutoExitLocked(now)) {
resetFloorExitCandidate()
return
}
const zoomOutIntentDelta = Math.max(maxDim * floorExitZoomOutIntentMinDeltaRatio, 1)
const isClearZoomOutIntent = distanceDelta > zoomOutIntentDelta
const isBeyondExitThreshold = distance > exitFloorAt
const baseExitThreshold = maxDim * props.autoSwitchThresholdHigh
const candidateExitAt = baseExitThreshold * floorExitCandidateThresholdMultiplier
const confirmExitAt = baseExitThreshold * floorExitConfirmThresholdMultiplier
const isBeyondCandidateThreshold = distance >= candidateExitAt
if (!isBeyondExitThreshold) {
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
if (!isBeyondCandidateThreshold) {
resetFloorExitCandidate()
return
}
if (isClearZoomOutIntent) {
floorExitZoomOutAccumulatedDistance += distanceDelta
if (!floorExitThresholdExceededSince) {
floorExitThresholdExceededSince = now
}
}
const requiredZoomOutDistance = Math.max(maxDim * floorExitZoomOutAccumulatedRatio, 8)
if (
!floorExitThresholdExceededSince
|| floorExitZoomOutAccumulatedDistance < requiredZoomOutDistance
) {
if (!floorExitCandidateStartedAt) {
floorExitCandidateStartedAt = now
floorExitCandidateBaseDistance = Math.max(previousDistance, distance, 1)
floorExitZoomOutAccumulatedRatio = 0
return
}
if (now - floorExitThresholdExceededSince >= floorExitThresholdHoldMs) {
// 从单楼层自动切换到完整外围模型
isAutoSwitchLocked = true
lastAutoSwitchTime = now
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
void runAutoSwitchLoad(
{
from: 'floor',
to: 'overview',
trigger: 'zoom-out',
distance
},
loadOverview,
'建筑外观模型加载失败'
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
},
loadOverview,
'建筑外观模型加载失败'
)
}
const runAutoSwitchLoad = async (
@@ -1974,7 +2131,7 @@ const runAutoSwitchLoad = async (
} catch (error) {
if (isStaleModelLoadError(error)) return
console.error('内 3D 自动视角切换失败:', error)
console.error('内 3D 自动视角切换失败:', error)
loadError.value = true
isLoading.value = false
setProgress(0, error instanceof Error ? error.message : fallbackMessage)
@@ -1996,10 +2153,6 @@ const disableAutoSwitchTemporarily = (durationMs: number) => {
}, durationMs)
}
const suppressAutoSwitchOnce = () => {
suppressNextAutoSwitchCheck = true
}
const setCameraView = (
center: THREE.Vector3,
maxDim: number,
@@ -2093,22 +2246,16 @@ const zoomCamera = (direction: 'in' | 'out', options: { source?: ZoomCameraSourc
if (!Number.isFinite(nextDistance) || nextDistance <= 0) return
const shouldSuppressAutoSwitch = options.source === 'button'
if (shouldSuppressAutoSwitch) {
suppressAutoSwitchOnce()
}
const shouldTrackUserZoom = options.source === 'button'
offset.setLength(nextDistance)
const nextPosition = controls.target.clone().add(offset)
moveCameraTo(nextPosition, controls.target, {
onComplete: shouldSuppressAutoSwitch
? () => {
suppressAutoSwitchOnce()
handleControlChange()
}
: handleControlChange
onComplete: handleControlChange
})
lastControlDistance = nextDistance
if (shouldTrackUserZoom) {
lastControlDistance = currentDistance
}
handleControlChange()
}
@@ -2166,9 +2313,10 @@ const attachCachedSharedModel = (
modelUrl: string,
name: string,
loadToken: number,
floorId?: string
floorId?: string,
options: { allowAnyCachedOverview?: boolean } = {}
) => {
if (!cachedOverviewModel || cachedSharedModelUrl !== modelUrl || !scene) return false
if (!cachedOverviewModel || (!options.allowAnyCachedOverview && cachedSharedModelUrl !== modelUrl) || !scene) return false
const targetScene = scene
activeModel = cachedOverviewModel
@@ -2186,8 +2334,12 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
if (!floor || !scene) return
const loadToken = startModelLoad()
const canUseCachedModel = canAttachCachedSharedModel(floor.modelUrl)
const preserveCurrentSceneUntilReady = options.preserveCurrentSceneUntilReady && !canUseCachedModel
const canUseExactCachedModel = canAttachCachedSharedModel(floor.modelUrl)
const canUseOverviewFallback = !canUseExactCachedModel
&& Boolean(options.allowOverviewFallback)
&& Boolean(options.preserveCurrentSceneUntilReady)
&& canAttachCachedOverviewModel()
const preserveCurrentSceneUntilReady = options.preserveCurrentSceneUntilReady && !canUseExactCachedModel
let nextModel: THREE.Object3D | null = null
if (!preserveCurrentSceneUntilReady) {
@@ -2196,63 +2348,90 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
clearSceneData()
}
if (canUseCachedModel) {
if (canUseExactCachedModel) {
activeView.value = 'floor'
currentFloor.value = floor.floorId
attachCachedSharedModel(floor.modelUrl, `GuideFloorModel_${floor.floorId}`, loadToken, floor.floorId)
} else {
if (!options.suppressProgress) {
setProgress(18, `正在加载 ${formatFloorLabel(floor.floorId)} 模型...`)
}
const gltf = await loadModel(
attachCachedSharedModel(
floor.modelUrl,
`正在加载 ${formatFloorLabel(floor.floorId)} 模型`,
`GuideFloorModel_${floor.floorId}`,
loadToken,
{ suppressProgress: options.suppressProgress }
floor.floorId
)
} else {
let gltf: GLTF | null = null
try {
assertCurrentModelLoad(loadToken, gltf.scene)
if (!options.suppressProgress) {
setProgress(18, `正在加载 ${formatFloorLabel(floor.floorId)} 模型...`)
}
gltf = await loadModel(
floor.modelUrl,
`正在加载 ${formatFloorLabel(floor.floorId)} 模型`,
loadToken,
{ suppressProgress: options.suppressProgress }
)
try {
assertCurrentModelLoad(loadToken, gltf.scene)
} catch (error) {
disposeObject(gltf.scene)
throw error
}
} catch (error) {
disposeObject(gltf.scene)
throw error
}
if (isStaleModelLoadError(error) || !canUseOverviewFallback) {
throw error
}
const targetScene = scene
if (!targetScene) {
disposeObject(gltf.scene)
throw createStaleModelLoadError()
}
nextModel = gltf.scene
nextModel.name = `GuideFloorModel_${floor.floorId}`
nextModel.userData.floorId = floor.floorId
prepareModel(nextModel)
try {
assertCurrentModelLoad(loadToken, nextModel)
} catch (error) {
nextModel = null
throw error
}
if (floor.sharedModelAsset) {
cacheAutoSwitchOverviewMaxDim(nextModel)
}
applyModelVisibilityForView(nextModel, 'floor', floor.floorId)
if (preserveCurrentSceneUntilReady) {
activeView.value = 'floor'
currentFloor.value = floor.floorId
clearSceneData()
attachCachedSharedModel(
floor.modelUrl,
`GuideFloorModel_${floor.floorId}`,
loadToken,
floor.floorId,
{ allowAnyCachedOverview: true }
)
}
activeModel = nextModel
if (floor.sharedModelAsset) {
cachedOverviewModel = activeModel
cachedSharedModelUrl = floor.modelUrl
if (gltf) {
const targetScene = scene
if (!targetScene) {
disposeObject(gltf.scene)
throw createStaleModelLoadError()
}
nextModel = gltf.scene
nextModel.name = `GuideFloorModel_${floor.floorId}`
nextModel.userData.floorId = floor.floorId
prepareModel(nextModel)
try {
assertCurrentModelLoad(loadToken, nextModel)
} catch (error) {
nextModel = null
throw error
}
if (floor.sharedModelAsset) {
cacheAutoSwitchOverviewMaxDim(nextModel)
}
applyModelVisibilityForView(nextModel, 'floor', floor.floorId)
if (preserveCurrentSceneUntilReady) {
activeView.value = 'floor'
currentFloor.value = floor.floorId
clearSceneData()
}
activeModel = nextModel
if (floor.sharedModelAsset) {
cachedOverviewModel = activeModel
cachedSharedModelUrl = floor.modelUrl
}
targetScene.add(activeModel)
}
targetScene.add(activeModel)
}
if (!activeModel) throw createStaleModelLoadError()
hasLoadedFloorViewOnce = true
fitCameraToObject(activeModel)
markFloorAutoSwitchEntry()
@@ -3142,7 +3321,7 @@ const queueTargetFocus = (request: TargetPoiFocusRequest | null) => {
}
const loadModelPackage = async () => {
setProgress(8, '正在读取内导览资源...')
setProgress(8, '正在读取内导览资源...')
const packageData = await props.modelSource.loadPackage()
renderPackage.value = packageData
@@ -3176,18 +3355,19 @@ const init3DScene = async () => {
isLoading.value = true
loadError.value = false
selectedPOI.value = null
hasLoadedFloorViewOnce = false
setProgress(0, '正在初始化三维场景...')
await initThree()
await loadModelPackage()
setProgress(100, '内三维模型加载完成')
setProgress(100, '内三维模型加载完成')
isLoading.value = false
queueTargetFocus(pendingTargetFocus || props.targetFocus || null)
} catch (error) {
if (isStaleModelLoadError(error)) return
console.error('内 3D 模型资源加载失败:', error)
console.error('内 3D 模型资源加载失败:', error)
loadError.value = true
isLoading.value = false
setProgress(0, error instanceof Error ? error.message : '请检查模型资源或网络状态后重试')
@@ -3269,7 +3449,7 @@ const disposeScene = () => {
const container = getContainerElement()
container?.removeEventListener('pointerdown', handlePointerDown)
container?.removeEventListener('pointerup', handlePointerUp)
container?.removeEventListener('wheel', handleWheelIntent)
renderer?.domElement.removeEventListener('wheel', handleWheelIntent, true)
resizeObserver?.disconnect()
resizeObserver = null
@@ -3277,6 +3457,7 @@ const disposeScene = () => {
// 清理自动切换相关资源
if (controls) {
controls.removeEventListener('start', handleControlStart)
controls.removeEventListener('end', handleControlEnd)
controls.removeEventListener('change', handleControlChange)
controls.dispose()
}
@@ -3292,10 +3473,11 @@ const disposeScene = () => {
cameraTween = null
isProgrammaticCameraChange = false
lastControlDistance = 0
suppressNextAutoSwitchCheck = false
floorAutoSwitchProtectedUntil = 0
floorExitThresholdExceededSince = 0
floorExitZoomOutAccumulatedDistance = 0
activeControlGestureStartedAt = 0
indoorEntryGestureLocked = false
indoorContextLockedUntil = 0
resetOverviewEntryIntent()
resetFloorExitCandidate()
clearSceneData()
disposePoiMarkerCache()
@@ -3393,7 +3575,7 @@ onUnmounted(() => {
width: 100%;
height: 100%;
overflow: hidden;
background: #f1f3ee;
background: #f7f8f4;
}
.three-canvas-wrapper {
@@ -3416,7 +3598,7 @@ onUnmounted(() => {
display: flex;
align-items: center;
justify-content: center;
background: rgba(241, 243, 238, 0.9);
background: rgba(247, 248, 244, 0.9);
z-index: 20;
}