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