From 200b54eade75c6bb11ccce3837e364067d345b8c Mon Sep 17 00:00:00 2001 From: lyf <2514544224@qq.com> Date: Sun, 19 Jul 2026 22:06:10 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=A5=BC=E5=B1=82=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=A7=86=E8=A7=89=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/map/ThreeMap.vue | 95 +++++++++----------- tests/e2e/three-map-camera-stability.spec.ts | 18 ++++ 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/src/components/map/ThreeMap.vue b/src/components/map/ThreeMap.vue index d95caaf..47b4c55 100644 --- a/src/components/map/ThreeMap.vue +++ b/src/components/map/ThreeMap.vue @@ -158,10 +158,18 @@ const OVERVIEW_INITIAL_CAMERA_PARAMS = { zoom: 1 } as const +// The source floor ID is only the calibration origin. This is the shared +// visitor-facing baseline for every ordinary floor, never a per-floor override. +const FLOOR_INITIAL_CAMERA_PARAMS = { + position: new THREE.Vector3(315.2911, 385.8819, 234.6114), + target: new THREE.Vector3(19.7236, -65.382, 42.1148), + zoom: 1 +} as const + const getIndoorInitialCameraDirection = () => ( - OVERVIEW_INITIAL_CAMERA_PARAMS.position + FLOOR_INITIAL_CAMERA_PARAMS.position .clone() - .sub(OVERVIEW_INITIAL_CAMERA_PARAMS.target) + .sub(FLOOR_INITIAL_CAMERA_PARAMS.target) .normalize() ) @@ -298,6 +306,32 @@ const referenceOverviewCameraState: CameraSnapshot = (() => { } })() +const referenceFloorCameraState: CameraSnapshot = (() => { + const referenceCamera = new THREE.PerspectiveCamera( + SGS_VISUAL_RENDER_CONFIG.camera.fov, + 1, + SGS_VISUAL_RENDER_CONFIG.camera.near, + SGS_VISUAL_RENDER_CONFIG.camera.far + ) + referenceCamera.position.copy(FLOOR_INITIAL_CAMERA_PARAMS.position) + referenceCamera.up.set(0, 1, 0) + referenceCamera.zoom = FLOOR_INITIAL_CAMERA_PARAMS.zoom + referenceCamera.lookAt(FLOOR_INITIAL_CAMERA_PARAMS.target) + referenceCamera.updateProjectionMatrix() + + return { + position: referenceCamera.position.clone(), + target: FLOOR_INITIAL_CAMERA_PARAMS.target.clone(), + up: referenceCamera.up.clone(), + quaternion: referenceCamera.quaternion.clone(), + fov: referenceCamera.fov, + zoom: referenceCamera.zoom, + near: referenceCamera.near, + far: referenceCamera.far, + distance: referenceCamera.position.distanceTo(FLOOR_INITIAL_CAMERA_PARAMS.target) + } +})() + type FloorIndexItem = GuideModelFloorAsset interface FloorOption { @@ -3498,44 +3532,7 @@ const getBoxSignature = (box: THREE.Box3) => [ 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, @@ -3543,17 +3540,7 @@ const createFloorViewBaseline = (floorId: string, model: THREE.Object3D, modelUr 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 - } + camera: cloneCameraSnapshot(referenceFloorCameraState) } } @@ -4243,7 +4230,7 @@ const commitPreparedFloorScene = ( const floorBaseline = options.applyFloorBaseline ? getFloorViewBaseline(expectedFloorId, activeModel, prepared.floor.modelUrl) : null - const cameraSnapshot = floorBaseline?.camera || options.cameraSnapshot || referenceOverviewCameraState + const cameraSnapshot = floorBaseline?.camera || options.cameraSnapshot || referenceFloorCameraState restoreCameraSnapshot(cameraSnapshot) if (!referenceBuildingAnchors.length) { updateReferenceBuildingAnchors(activeModel) @@ -4292,7 +4279,9 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => { return false } - const cameraSnapshot = cloneCameraSnapshot(options.cameraSnapshot || captureCameraSnapshot()) + const cameraSnapshot = options.cameraSnapshot + ? cloneCameraSnapshot(options.cameraSnapshot) + : undefined const loadToken = startFloorContextTransaction(requestedFloorId) if (options.detachPoiBeforeLoad) { detachActivePoiLayer() @@ -4303,7 +4292,7 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => { assertCurrentFloorContextTransaction(loadToken, requestedFloorId, prepared.ownsModel ? prepared.model : undefined) commitPreparedFloorScene(prepared, loadToken, requestedFloorId, { ...options, - cameraSnapshot + ...(cameraSnapshot ? { cameraSnapshot } : {}) }) return true } catch (error) { diff --git a/tests/e2e/three-map-camera-stability.spec.ts b/tests/e2e/three-map-camera-stability.spec.ts index dd35e3c..ff2854e 100644 --- a/tests/e2e/three-map-camera-stability.spec.ts +++ b/tests/e2e/three-map-camera-stability.spec.ts @@ -116,6 +116,20 @@ const getReport = async (page: Page) => { return state!.report } +const FLOOR_CAMERA_BASELINE = { + position: [315.2911, 385.8819, 234.6114] as const, + target: [19.7236, -65.382, 42.1148] as const, + zoom: 1, + distance: 572.7601 +} + +const assertFloorCameraBaseline = (camera: CameraReport) => { + expect(getVectorDelta(camera.position, FLOOR_CAMERA_BASELINE.position)).toBeLessThan(1e-4) + expect(getVectorDelta(camera.target, FLOOR_CAMERA_BASELINE.target)).toBeLessThan(1e-4) + expect(camera.zoom).toBe(FLOOR_CAMERA_BASELINE.zoom) + expect(Math.abs(camera.distance - FLOOR_CAMERA_BASELINE.distance)).toBeLessThan(1e-3) +} + const openGuide = async (page: Page) => { await page.goto('/') await page.waitForURL(/tab=guide/, { timeout: 30_000 }) @@ -479,6 +493,9 @@ test('floor view baseline is deterministic across repeated resets', async ({ pag return api.getFloorBaseline(floorId) }, floor.floorId) expect(baseline, `missing floor baseline ${floor.floorId}`).not.toBeNull() + assertFloorCameraBaseline(baseline!) + assertFloorCameraBaseline(first.camera) + expect(first.modelRoot!.maxError).toBeLessThan(1e-12) expect(getMaximumCameraDelta(baseline!, first.camera)).toBeLessThan(1e-6) await page.evaluate(async (floorId) => { const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi }) @@ -489,6 +506,7 @@ test('floor view baseline is deterministic across repeated resets', async ({ pag const second = await getReport(page) expect(second.activeView).toBe('floor') expect(second.floorId).toBe(floor.floorId) + assertFloorCameraBaseline(second.camera) expect(getMaximumCameraDelta(first.camera, second.camera)).toBeLessThan(1e-6) expect(getMaximumCameraDelta(baseline!, second.camera)).toBeLessThan(1e-6) }