From cc3d61a9609758c3f1816a2d1830e598a0284131 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Tue, 3 Nov 2026 09:29:22 -0800 Subject: [PATCH] fix(camera): keep the zoom ceiling fixed while tracking a target While the camera tracks an entity or looks at a point, Cesium measures zoom distance from the target, not the Earth. Raising the ceiling to the camera's altitude then lifted it with every outward step, so a tracked camera could zoom out without limit. The guard now follows the altitude only for an untracked 3D camera and otherwise keeps its 40,000 km ceiling. --- src/app/zoomOutGuard.js | 7 ++++++- src/app/zoomOutGuard.test.mjs | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/app/zoomOutGuard.js b/src/app/zoomOutGuard.js index 5457b509b..51d2fdfcd 100644 --- a/src/app/zoomOutGuard.js +++ b/src/app/zoomOutGuard.js @@ -111,7 +111,12 @@ export function installZoomOutGuard( const followCeiling = () => { if (!ownsCeiling || controller.maximumZoomDistance !== maximumZoomDistance) return; - const height = camera.positionCartographic?.height; + // While tracking, Cesium measures zoom from the target, not the Earth, so + // the camera's altitude would lift the ceiling with every outward step. + const untracked = + scene.mode === Cesium.SceneMode.SCENE3D && + Cesium.Matrix4.equals(camera.transform, Cesium.Matrix4.IDENTITY); + const height = untracked ? camera.positionCartographic?.height : undefined; maximumZoomDistance = height > MAXIMUM_ZOOM_DISTANCE_M ? height : MAXIMUM_ZOOM_DISTANCE_M; controller.maximumZoomDistance = maximumZoomDistance; diff --git a/src/app/zoomOutGuard.test.mjs b/src/app/zoomOutGuard.test.mjs index 1b2322cb5..4785cee8b 100644 --- a/src/app/zoomOutGuard.test.mjs +++ b/src/app/zoomOutGuard.test.mjs @@ -741,3 +741,24 @@ test('a share-link view restored above the ceiling is honoured by later frames', s.dispose(); } }); + +test('a tracked camera cannot climb past the ceiling from its target', () => { + const s = realControllerScene(1_000_000); + try { + s.frame(); + // In look-at mode Cesium measures zoom from the target, not the Earth. + s.camera.lookAt( + Cesium.Cartesian3.fromDegrees(0, 0, 35_786_000), + new Cesium.HeadingPitchRange(0, -0.5, 5_000_000), + ); + for (let i = 0; i < 20; i++) { + s.wheel(100); + s.frame(); + } + const range = Cesium.Cartesian3.magnitude(s.camera.position); + assert.ok(range <= MAXIMUM_ZOOM_DISTANCE_M + 1_000, `${range} m`); + assert.equal(s.controller.maximumZoomDistance, MAXIMUM_ZOOM_DISTANCE_M); + } finally { + s.dispose(); + } +});