统一楼层模型视觉基线
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-19 22:06:10 +08:00
parent 47dd4c7c87
commit 200b54eade
2 changed files with 60 additions and 53 deletions

View File

@@ -158,10 +158,18 @@ const OVERVIEW_INITIAL_CAMERA_PARAMS = {
zoom: 1 zoom: 1
} as const } 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 = () => ( const getIndoorInitialCameraDirection = () => (
OVERVIEW_INITIAL_CAMERA_PARAMS.position FLOOR_INITIAL_CAMERA_PARAMS.position
.clone() .clone()
.sub(OVERVIEW_INITIAL_CAMERA_PARAMS.target) .sub(FLOOR_INITIAL_CAMERA_PARAMS.target)
.normalize() .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 type FloorIndexItem = GuideModelFloorAsset
interface FloorOption { interface FloorOption {
@@ -3498,44 +3532,7 @@ const getBoxSignature = (box: THREE.Box3) => [
const createFloorViewBaseline = (floorId: string, model: THREE.Object3D, modelUrl: string): FloorViewBaseline | null => { const createFloorViewBaseline = (floorId: string, model: THREE.Object3D, modelUrl: string): FloorViewBaseline | null => {
if (!camera || !controls) return null if (!camera || !controls) return null
const overviewCamera = initialGuideState?.camera || referenceOverviewCameraState
const box = getObjectBox(model) 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 { return {
floorId, floorId,
@@ -3543,17 +3540,7 @@ const createFloorViewBaseline = (floorId: string, model: THREE.Object3D, modelUr
packageEpoch: modelPackageEpoch, packageEpoch: modelPackageEpoch,
aspectRatio: camera.aspect, aspectRatio: camera.aspect,
boundsSignature: getBoxSignature(box), boundsSignature: getBoxSignature(box),
camera: { camera: cloneCameraSnapshot(referenceFloorCameraState)
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
}
} }
} }
@@ -4243,7 +4230,7 @@ const commitPreparedFloorScene = (
const floorBaseline = options.applyFloorBaseline const floorBaseline = options.applyFloorBaseline
? getFloorViewBaseline(expectedFloorId, activeModel, prepared.floor.modelUrl) ? getFloorViewBaseline(expectedFloorId, activeModel, prepared.floor.modelUrl)
: null : null
const cameraSnapshot = floorBaseline?.camera || options.cameraSnapshot || referenceOverviewCameraState const cameraSnapshot = floorBaseline?.camera || options.cameraSnapshot || referenceFloorCameraState
restoreCameraSnapshot(cameraSnapshot) restoreCameraSnapshot(cameraSnapshot)
if (!referenceBuildingAnchors.length) { if (!referenceBuildingAnchors.length) {
updateReferenceBuildingAnchors(activeModel) updateReferenceBuildingAnchors(activeModel)
@@ -4292,7 +4279,9 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
return false return false
} }
const cameraSnapshot = cloneCameraSnapshot(options.cameraSnapshot || captureCameraSnapshot()) const cameraSnapshot = options.cameraSnapshot
? cloneCameraSnapshot(options.cameraSnapshot)
: undefined
const loadToken = startFloorContextTransaction(requestedFloorId) const loadToken = startFloorContextTransaction(requestedFloorId)
if (options.detachPoiBeforeLoad) { if (options.detachPoiBeforeLoad) {
detachActivePoiLayer() detachActivePoiLayer()
@@ -4303,7 +4292,7 @@ const loadFloor = async (floorId: string, options: LoadFloorOptions = {}) => {
assertCurrentFloorContextTransaction(loadToken, requestedFloorId, prepared.ownsModel ? prepared.model : undefined) assertCurrentFloorContextTransaction(loadToken, requestedFloorId, prepared.ownsModel ? prepared.model : undefined)
commitPreparedFloorScene(prepared, loadToken, requestedFloorId, { commitPreparedFloorScene(prepared, loadToken, requestedFloorId, {
...options, ...options,
cameraSnapshot ...(cameraSnapshot ? { cameraSnapshot } : {})
}) })
return true return true
} catch (error) { } catch (error) {

View File

@@ -116,6 +116,20 @@ const getReport = async (page: Page) => {
return state!.report 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) => { const openGuide = async (page: Page) => {
await page.goto('/') await page.goto('/')
await page.waitForURL(/tab=guide/, { timeout: 30_000 }) 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) return api.getFloorBaseline(floorId)
}, floor.floorId) }, floor.floorId)
expect(baseline, `missing floor baseline ${floor.floorId}`).not.toBeNull() 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) expect(getMaximumCameraDelta(baseline!, first.camera)).toBeLessThan(1e-6)
await page.evaluate(async (floorId) => { await page.evaluate(async (floorId) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi }) 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) const second = await getReport(page)
expect(second.activeView).toBe('floor') expect(second.activeView).toBe('floor')
expect(second.floorId).toBe(floor.floorId) expect(second.floorId).toBe(floor.floorId)
assertFloorCameraBaseline(second.camera)
expect(getMaximumCameraDelta(first.camera, second.camera)).toBeLessThan(1e-6) expect(getMaximumCameraDelta(first.camera, second.camera)).toBeLessThan(1e-6)
expect(getMaximumCameraDelta(baseline!, second.camera)).toBeLessThan(1e-6) expect(getMaximumCameraDelta(baseline!, second.camera)).toBeLessThan(1e-6)
} }