统一楼层视觉基线与详情返回复位
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-14 23:17:33 +08:00
parent 84fbbc0694
commit c08e1afe85
6 changed files with 274 additions and 34 deletions

View File

@@ -209,6 +209,7 @@ interface LoadFloorOptions {
detachPoiBeforeLoad?: boolean
allowSameFloorReload?: boolean
cameraSnapshot?: CameraSnapshot
applyFloorBaseline?: boolean
onCameraStable?: () => void
}
@@ -243,6 +244,21 @@ interface CameraSnapshot {
distance: number
}
type ResetViewBaselineOptions = {
view: 'overview' | 'floor'
floorId?: string
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
}
interface FloorViewBaseline {
floorId: string
modelUrl: string
packageEpoch: number
aspectRatio: number
boundsSignature: string
camera: CameraSnapshot
}
// 所有普通楼层/外观切换共享该外观基准;切换时使用完整快照恢复而非包围盒拟合。
const referenceOverviewCameraState: CameraSnapshot = (() => {
const referenceCamera = new THREE.PerspectiveCamera(
@@ -517,6 +533,8 @@ let firstModelLoadStartedAt = 0
let firstModelVisibleReported = false
let initialModelSettled = false
let initialGuideState: { floorId: string; camera: CameraSnapshot } | null = null
const floorViewBaselines = new Map<string, FloorViewBaseline>()
let modelPackageEpoch = 0
const cameraTweenEnabled = true
const cameraTweenDurationMs = 480
@@ -1983,7 +2001,9 @@ const handleResize = () => {
const width = Math.max(1, container.clientWidth)
const height = Math.max(1, container.clientHeight)
camera.aspect = width / height
const nextAspect = width / height
if (Math.abs(camera.aspect - nextAspect) > 1e-6) clearFloorViewBaselines()
camera.aspect = nextAspect
camera.updateProjectionMatrix()
renderer.setSize(width, height)
}
@@ -3390,6 +3410,99 @@ const applyReferenceOverviewCameraState = () => {
restoreCameraSnapshot(referenceOverviewCameraState)
}
const clearFloorViewBaselines = () => {
floorViewBaselines.clear()
}
const getBoxSignature = (box: THREE.Box3) => [
box.min.x,
box.min.y,
box.min.z,
box.max.x,
box.max.y,
box.max.z
].map((value) => value.toFixed(6)).join(':')
const createFloorViewBaseline = (floorId: string, model: THREE.Object3D, modelUrl: string): FloorViewBaseline | null => {
if (!camera || !controls) return null
const overviewCamera = initialGuideState?.camera || referenceOverviewCameraState
const box = getObjectBox(model)
const center = box.getCenter(new THREE.Vector3())
const size = box.getSize(new THREE.Vector3())
const maxDim = Math.max(size.x, size.y, size.z, 1)
const verticalFov = overviewCamera.fov * (Math.PI / 180)
const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * camera.aspect)
const visibleFraction = FLOOR_CAMERA_DISTANCE_FACTOR
const distance = Math.max(
size.y / (2 * Math.tan(verticalFov / 2) * visibleFraction),
size.x / (2 * Math.tan(horizontalFov / 2) * visibleFraction),
size.z / (2 * Math.tan(verticalFov / 2) * visibleFraction),
1
)
const direction = overviewCamera.position.clone().sub(overviewCamera.target).normalize()
const target = center.clone()
const position = target.clone().add(direction.multiplyScalar(distance))
const offsetRatio = SGS_VISUAL_RENDER_CONFIG.framing.floorScreenOffsetRatio
const viewDirection = target.clone().sub(position).normalize()
const cameraRight = new THREE.Vector3().crossVectors(viewDirection, overviewCamera.up).normalize()
const viewHeight = 2 * distance * Math.tan(verticalFov / 2)
const viewWidth = viewHeight * camera.aspect
const screenOffset = cameraRight.multiplyScalar(offsetRatio.x * viewWidth)
.add(overviewCamera.up.clone().multiplyScalar(offsetRatio.y * viewHeight))
position.add(screenOffset)
target.add(screenOffset)
const baselineCamera = new THREE.PerspectiveCamera(
overviewCamera.fov,
camera.aspect,
Math.max(SGS_VISUAL_RENDER_CONFIG.camera.near, maxDim / 1000),
Math.max(SGS_VISUAL_RENDER_CONFIG.camera.far, maxDim * 20)
)
baselineCamera.position.copy(position)
baselineCamera.up.copy(overviewCamera.up)
baselineCamera.zoom = overviewCamera.zoom
baselineCamera.lookAt(target)
baselineCamera.updateProjectionMatrix()
return {
floorId,
modelUrl,
packageEpoch: modelPackageEpoch,
aspectRatio: camera.aspect,
boundsSignature: getBoxSignature(box),
camera: {
position: baselineCamera.position.clone(),
target,
up: baselineCamera.up.clone(),
quaternion: baselineCamera.quaternion.clone(),
fov: baselineCamera.fov,
zoom: baselineCamera.zoom,
near: baselineCamera.near,
far: baselineCamera.far,
distance
}
}
}
const getFloorViewBaseline = (floorId: string, model: THREE.Object3D, modelUrl: string) => {
const cached = floorViewBaselines.get(floorId)
const boundsSignature = getBoxSignature(getObjectBox(model))
if (
cached
&& cached.modelUrl === modelUrl
&& cached.packageEpoch === modelPackageEpoch
&& cached.boundsSignature === boundsSignature
&& Math.abs(cached.aspectRatio - (camera?.aspect || 1)) < 1e-6
) {
return cached
}
const baseline = createFloorViewBaseline(floorId, model, modelUrl)
if (baseline) floorViewBaselines.set(floorId, baseline)
return baseline || null
}
const serializeCameraSnapshot = (snapshot: CameraSnapshot) => ({
position: { x: snapshot.position.x, y: snapshot.position.y, z: snapshot.position.z },
target: { x: snapshot.target.x, y: snapshot.target.y, z: snapshot.target.z },
@@ -3542,6 +3655,10 @@ const installVisualStabilityDiagnostics = () => {
label: floor.label,
modelMatchKeys: floor.modelMatchKeys || []
})),
getFloorBaseline: (floorId: string) => {
const baseline = floorViewBaselines.get(floorId)
return baseline ? serializeCameraSnapshot(baseline.camera) : null
},
switchFloor: handleFloorChange,
showOverview,
resetCamera,
@@ -3563,6 +3680,7 @@ const installVisualStabilityDiagnostics = () => {
return targetFocusQueue
},
clearTargetFocus,
resetToViewBaseline,
resetToInitialState
}
}
@@ -4003,7 +4121,10 @@ const commitPreparedFloorScene = (
selectedPOI.value = null
updatePoiMarkerFocus()
hasLoadedFloorViewOnce = true
const cameraSnapshot = options.cameraSnapshot || referenceOverviewCameraState
const floorBaseline = options.applyFloorBaseline
? getFloorViewBaseline(expectedFloorId, activeModel, prepared.floor.modelUrl)
: null
const cameraSnapshot = floorBaseline?.camera || options.cameraSnapshot || referenceOverviewCameraState
restoreCameraSnapshot(cameraSnapshot)
if (!referenceBuildingAnchors.length) {
updateReferenceBuildingAnchors(activeModel)
@@ -5272,6 +5393,8 @@ const queueTargetFocus = (request: TargetPoiFocusRequest | null) => {
const loadModelPackage = async () => {
setProgress(8, '正在读取馆内导览资源...')
const packageData = await props.modelSource.loadPackage()
modelPackageEpoch += 1
clearFloorViewBaselines()
renderPackage.value = packageData
setProgress(14, '正在读取楼层索引...')
@@ -5314,6 +5437,7 @@ const loadModelPackage = async () => {
const init3DScene = async () => {
try {
disposeScene()
clearFloorViewBaselines()
isDisposed = false
firstModelLoadStartedAt = getNow()
firstModelVisibleReported = false
@@ -5461,7 +5585,7 @@ const showMultiFloor = async () => {
// Restores business and camera state on the existing scene. It never initializes WebGL or
// reloads the model package, so closing a detail page cannot remount ThreeMap.
const resetToInitialState = async () => {
const resetToViewBaseline = async (options: ResetViewBaselineOptions) => {
clearTargetFocus()
invalidateModelLoads()
adjacentPreloadSeq += 1
@@ -5476,7 +5600,7 @@ const resetToInitialState = async () => {
disposeFocusBase()
clearFocusHallHighlight()
updatePoiMarkerFocus()
autoSwitchStateMachine.reset('overview')
autoSwitchStateMachine.reset(options.view)
resetInteractionGateState()
const baseline = initialGuideState
@@ -5484,15 +5608,38 @@ const resetToInitialState = async () => {
isLoading.value = true
try {
currentFloor.value = baseline.floorId
if (activeView.value !== 'overview') {
await loadOverview({ cameraSnapshot: baseline.camera })
if (options.view === 'floor') {
const floorId = options.floorId
if (!floorId) return false
const floor = floorIndex.value.find((item) => item.floorId === floorId)
if (!floor) return false
if (activeView.value !== 'floor' || currentFloor.value !== floorId || activeModel?.userData.floorId !== floorId) {
await loadFloor(floorId, { applyFloorBaseline: true, detachPoiBeforeLoad: true })
} else {
const floorBaseline = getFloorViewBaseline(floorId, activeModel, floor.modelUrl)
if (!floorBaseline) return false
restoreCameraSnapshot(floorBaseline.camera)
}
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return false
activeView.value = 'floor'
currentFloor.value = floorId
autoSwitchStateMachine.reset('floor')
} else {
currentFloor.value = baseline.floorId
if (activeView.value !== 'overview') {
await loadOverview({ cameraSnapshot: baseline.camera })
}
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return false
restoreCameraSnapshot(baseline.camera)
currentFloor.value = baseline.floorId
activeView.value = 'overview'
autoSwitchStateMachine.reset('overview')
}
if (controls && activeView.value === 'floor') {
ensureFloorAutoExitZoomRange()
autoSwitchStateMachine.setFloorInitialDistance(controls.getDistance())
}
if (!scene || !isCurrentModelLoad(modelLoadVersion)) return false
restoreCameraSnapshot(baseline.camera)
currentFloor.value = baseline.floorId
activeView.value = 'overview'
autoSwitchStateMachine.reset('overview')
refreshPoiVisibilityByDistance()
return true
} catch (error) {
@@ -5505,6 +5652,11 @@ const resetToInitialState = async () => {
}
}
const resetToInitialState = () => resetToViewBaseline({
view: 'overview',
reason: 'manual-reset'
})
const retryLoad = () => {
init3DScene()
}
@@ -5595,6 +5747,7 @@ defineExpose({
},
clearSelection: clearMapSelection,
clearRoute: clearRoutePreview,
resetToViewBaseline,
resetToInitialState,
clearNavigation: () => clearMapSelection(false),
disableAutoSwitchTemporarily

View File

@@ -460,6 +460,11 @@ const indoorRendererRef = ref<{
zoomCamera?: (direction: 'in' | 'out', options?: { source?: 'button' | 'gesture' }) => void
clearSelection?: (shouldEmit?: boolean) => void
clearRoute?: () => Promise<void> | void
resetToViewBaseline?: (options: {
view: 'overview' | 'floor'
floorId?: string
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
}) => Promise<boolean> | boolean
resetToInitialState?: () => Promise<boolean> | boolean
disableAutoSwitchTemporarily?: (durationMs: number) => void
} | null>(null)
@@ -781,6 +786,11 @@ defineExpose({
indoorRendererRef.value?.clearRoute?.()
},
showOverview: handleShowOverview,
resetToViewBaseline: (options: {
view: 'overview' | 'floor'
floorId?: string
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
}) => indoorRendererRef.value?.resetToViewBaseline?.(options) || false,
resetToInitialState: () => indoorRendererRef.value?.resetToInitialState?.() || false
})
</script>