fix(camera): run a recovery check a scene deferred once it ends

A check skipped because a flight or a playing Director scene was in
progress used to be dropped, so a run that ended or held on a stars-only
shot stayed there until the next camera move. Keep watching instead:
the settle check retries once the flight or scene ends, within its
existing five-minute bound, and a moveEnd that arrives while it is
deferred keeps the check armed rather than stopping it.

Also test that the free-look limit never raises Cesium's trackball
height and that dispose keeps a value set after install, note in its
JSDoc the enableLook case it cannot tell apart, and document the
trackball change and the retry in CURRENT-STATE and the changelog.
This commit is contained in:
Manjunath committed 2026-11-05 14:36:10 +05:30
1 parent 2f9d13f39d
commit 29935cc094
4 files changed
+76 -31

No files matched your search

+6 -5
View File
@@ -1,8 +1,9 @@
- Keep the globe in view from orbit. Cesium's free look (Shift+drag) turned - Keep the globe in view from orbit. Cesium's free look (Shift+drag) turned
the camera in place, so a few drags from the whole-globe view left only the camera in place, so a few drags from the whole-globe view left only
stars, and the pose stayed in the share link. Free look now works only below stars, and the pose stayed in the share link. Free look now works only below
1,000 km; a camera move that ends above it with no planet surface in view 1,000 km, and above it a drag that starts on the stars orbits the globe
(inside the scope keyhole when the scope is on) turns straight down; and instead of turning the camera; a camera move that ends above it with no
voice tilt and turns from orbit stop while the planet is still on screen and planet surface in view (inside the scope keyhole when the scope is on) turns
say so instead of reporting a turn they could not make (Manjunath, straight down; and voice tilt and turns from orbit stop while the planet is
@manjunath22466, #1532). still on screen and say so instead of reporting a turn they could not make
(Manjunath, @manjunath22466, #1532).
+17 -15
View File
@@ -67,10 +67,11 @@ the real browser controller at desktop and phone widths with keyless fixtures.
Cesium's free look (Shift+drag, which turns the camera in place) works only Cesium's free look (Shift+drag, which turns the camera in place) works only
below 1,000 km. Above that height a few drags could turn the whole planet out of below 1,000 km. Above that height a few drags could turn the whole planet out of
frame and leave only stars, so Shift+drag does nothing there. Cesium also frame and leave only stars, so Shift+drag does nothing there. Cesium also turns
turns the camera in place for a drag that starts on the stars below its the camera in place for a drag that starts on the stars below its trackball
trackball height (7,500 km by default); that height is lowered to 1,000 km, so height (7,500 km by default); that height is lowered to 1,000 km, so such a drag
such a drag orbits the globe above it, as it does higher up. Orbit, tilt and orbits the globe above it, as it does higher up, and a tilt drag from the stars
there tilts about the planet's edge or does nothing. Otherwise orbit, tilt and
zoom are unchanged at every height. How much of the frame shows the planet's zoom are unchanged at every height. How much of the frame shows the planet's
surface is measured by casting a 32×18 grid of view rays at the ellipsoid; while surface is measured by casting a 32×18 grid of view rays at the ellipsoid; while
the scope mask is on (the default) only the rays inside its keyhole count, since the scope mask is on (the default) only the rays inside its keyhole count, since
@@ -97,17 +98,18 @@ rounding error every frame and Cesium never sees it stop. That check keeps
watching until move-end arrives, recovering once after each move that comes to watching until move-end arrives, recovering once after each move that comes to
rest, and gives up five minutes after the camera last moved. Limb glow, and a rest, and gives up five minutes after the camera last moved. Limb glow, and a
surface sliver up to about one grid cell thick (some 50 px on a 900 px tall surface sliver up to about one grid cell thick (some 50 px on a 900 px tall
frame), can count as out of view. The zoom-out guard above keeps wheel and frame), can count as out of view. The zoom-out guard above keeps wheel and pinch
pinch zoom-outs in view; this catches the other ways a move can end looking zoom-outs in view; this catches the other ways a move can end looking away, such
away, such as a saved or shared view that did. as a saved or shared view that did. Views with more surface than that, such as
Views with more surface than that, such as the oblique orbital Director shots, the oblique orbital Director shots, are left as they are, and so are tracked or
are left as they are, and so are tracked or locked cameras, a camera flight in locked cameras, a camera flight in progress, a Director scene while it plays
progress, a Director scene while it plays (holds included) and the 2D and (holds included) and the 2D and Columbus modes; a check a flight or scene defers
Columbus modes. A view that loses its surface without a camera move (a resize, runs once it ends. A Director shot loaded or replayed outside a run is not a
or turning the scope on) is left as it is until the next move. A voice turn playing scene. A view that loses its surface without a camera move (a resize, or
requested while the camera is tracked is judged turning the scope on) is left as it is until the next move. A voice turn
by the pose voice's release of the follow camera will leave, before that requested while the camera is tracked is judged by the pose voice's release of
release, so a refused turn keeps the tracking. the follow camera will leave, before that release, so a refused turn keeps the
tracking.
Analyst region queries resolve unique Natural Earth names offline without Analyst region queries resolve unique Natural Earth names offline without
geocoding, so Gold Coast and Ivory Coast name the West African natural regions. geocoding, so Gold Coast and Ivory Coast name the West African natural regions.
+38 -1
View File
@@ -412,12 +412,24 @@ test('drags that start on the stars orbit above the free-look height', () => {
const lower = freeLookFixture({ height: 500 }); const lower = freeLookFixture({ height: 500 });
lower.controller.minimumTrackBallHeight = 500_000; lower.controller.minimumTrackBallHeight = 500_000;
installFreeLookHeightLimit(lower.viewer)(); const disposeLower = installFreeLookHeightLimit(lower.viewer);
assert.equal( assert.equal(
lower.controller.minimumTrackBallHeight, lower.controller.minimumTrackBallHeight,
500_000, 500_000,
'never raised', 'never raised',
); );
disposeLower();
assert.equal(lower.controller.minimumTrackBallHeight, 500_000);
const later = freeLookFixture({ height: 500 });
const disposeLater = installFreeLookHeightLimit(later.viewer);
later.controller.minimumTrackBallHeight = 2_000_000;
disposeLater();
assert.equal(
later.controller.minimumTrackBallHeight,
2_000_000,
'dispose keeps a value set after install',
);
}); });
test('free look keeps a setting other code makes after it is installed', () => { test('free look keeps a setting other code makes after it is installed', () => {
@@ -739,6 +751,31 @@ test('the recovery leaves a camera flight and an authored scene alone', () => {
assert.ok(Cesium.Math.equalsEpsilon(camera.pitch, -Math.PI / 2, 1e-6)); assert.ok(Cesium.Math.equalsEpsilon(camera.pitch, -Math.PI / 2, 1e-6));
}); });
test('a check a scene or flight defers runs once it ends', () => {
for (const viaMoveEnd of [false, true]) {
let playing = true;
const timers = fakeTimers();
const { camera, viewer } = realCamera({ width: 1225, height: 923 });
installGlobeInViewRecovery(viewer, {
paused: () => playing,
...timers,
});
aim(camera, reportedStuckView);
camera.moveStart.raiseEvent();
// A hold on a stars-only shot: quiet, with or without Cesium's moveEnd.
for (let i = 0; i < 4; i += 1) timers.tick();
if (viaMoveEnd) camera.moveEnd.raiseEvent();
assert.equal(globeOutOfFrame(camera), true, 'left alone while it plays');
assert.equal(timers.pending(), 1, 'still watching');
playing = false;
timers.tick();
assert.ok(
Cesium.Math.equalsEpsilon(camera.pitch, -Math.PI / 2, 1e-6),
`recovered once the scene ends (${viaMoveEnd ? 'after moveEnd' : 'no moveEnd'})`,
);
}
});
function fakeTimers() { function fakeTimers() {
let next = 1; let next = 1;
const pending = new Map(); const pending = new Map();
+15 -10
View File
@@ -112,7 +112,9 @@ export function installTrackpadPinchZoom(
* Allow Cesium's free look only below `maxHeight` and return its disposer. * Allow Cesium's free look only below `maxHeight` and return its disposer.
* Orbit, tilt and zoom stay available at every height. A value other code * Orbit, tilt and zoom stay available at every height. A value other code
* sets later is kept as the one to allow below that height, and restored on * sets later is kept as the one to allow below that height, and restored on
* dispose. `enableLook` gates the Shift+drag binding only: below its trackball * dispose; a `false` written while the camera is above `maxHeight` cannot be
* told apart from this limit's own write, so dispose restores the earlier
* value then. `enableLook` gates the Shift+drag binding only: below its trackball
* height Cesium also turns the camera in place for a drag that starts on the * height Cesium also turns the camera in place for a drag that starts on the
* stars. That height is lowered to `maxHeight` too, so above it such a drag * stars. That height is lowered to `maxHeight` too, so above it such a drag
* orbits the globe, as it does above Cesium's own 7,500 km. * orbits the globe, as it does above Cesium's own 7,500 km.
@@ -171,7 +173,8 @@ const SETTLE_GIVE_UP_MS = 5 * 60_000;
* looking away from the planet. * looking away from the planet.
* Heading and position are kept. Tracked cameras, locked ones (non-identity * Heading and position are kept. Tracked cameras, locked ones (non-identity
* transform), a camera flight in progress, the 2D and Columbus modes, and any * transform), a camera flight in progress, the 2D and Columbus modes, and any
* time `paused` returns true (an authored scene playing) are left alone. * time `paused` returns true (an authored scene playing) are left alone; a
* check they defer runs again once they end, within the settle check's bound.
* `keyholeRadius` * `keyholeRadius`
* returns the radius of the visible circle when a scope mask hides the rest of * returns the radius of the visible circle when a scope mask hides the rest of
* the frame (see `globeFrameShare`), or 0; it is read at every check. * the frame (see `globeFrameShare`), or 0; it is read at every check.
@@ -202,16 +205,18 @@ export function installGlobeInViewRecovery(
if (!scene || !camera?.moveEnd || !camera?.moveStart) if (!scene || !camera?.moveEnd || !camera?.moveStart)
throw new TypeError('A complete Cesium viewer is required'); throw new TypeError('A complete Cesium viewer is required');
// Returns false while a flight or a paused scene defers the check, so the
// caller can try again once it ends.
const recover = () => { const recover = () => {
if (!isFreeGlobeCamera(viewer)) return; if (!isFreeGlobeCamera(viewer)) return true;
// Cesium has no public running-flight flag. // Cesium has no public running-flight flag.
if (camera._currentFlight || paused()) return; if (camera._currentFlight || paused()) return false;
const height = camera.positionCartographic?.height; const height = camera.positionCartographic?.height;
if ( if (
!(height > minHeight) || !(height > minHeight) ||
!globeOutOfFrame(camera, { keyholeRadius: keyholeRadius() }) !globeOutOfFrame(camera, { keyholeRadius: keyholeRadius() })
) )
return; return true;
camera.setView({ camera.setView({
orientation: { orientation: {
heading: camera.heading, heading: camera.heading,
@@ -220,6 +225,7 @@ export function installGlobeInViewRecovery(
}, },
}); });
scene.requestRender?.(); scene.requestRender?.();
return true;
}; };
const lastPosition = new Cesium.Cartesian3(); const lastPosition = new Cesium.Cartesian3();
@@ -255,10 +261,8 @@ export function installGlobeInViewRecovery(
} }
// Two quiet samples in a row after a move: at rest for half a second, as // Two quiet samples in a row after a move: at rest for half a second, as
// for moveEnd. // for moveEnd.
if (restingSamples >= 2 && movedSinceCheck) { if (restingSamples >= 2 && movedSinceCheck && recover())
movedSinceCheck = false; movedSinceCheck = false;
recover();
}
if (settleElapsed >= SETTLE_GIVE_UP_MS) return; if (settleElapsed >= SETTLE_GIVE_UP_MS) return;
Cesium.Cartesian3.clone(camera.positionWC, lastPosition); Cesium.Cartesian3.clone(camera.positionWC, lastPosition);
Cesium.Cartesian3.clone(camera.directionWC, lastDirection); Cesium.Cartesian3.clone(camera.directionWC, lastDirection);
@@ -274,8 +278,9 @@ export function installGlobeInViewRecovery(
settleTimer = schedule(sample, SETTLE_SAMPLE_MS); settleTimer = schedule(sample, SETTLE_SAMPLE_MS);
}; };
const removeMoveEnd = camera.moveEnd.addEventListener(() => { const removeMoveEnd = camera.moveEnd.addEventListener(() => {
stopSettling(); if (recover()) stopSettling();
recover(); // Deferred: keep sampling so the check runs once the flight or scene ends.
else if (settleTimer === null) startSettling();
}); });
const removeMoveStart = camera.moveStart.addEventListener(startSettling); const removeMoveStart = camera.moveStart.addEventListener(startSettling);