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

+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 });
lower.controller.minimumTrackBallHeight = 500_000;
installFreeLookHeightLimit(lower.viewer)();
const disposeLower = installFreeLookHeightLimit(lower.viewer);
assert.equal(
lower.controller.minimumTrackBallHeight,
500_000,
'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', () => {
@@ -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));
});
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() {
let next = 1;
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.
* 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
* 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
* 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.
@@ -171,7 +173,8 @@ const SETTLE_GIVE_UP_MS = 5 * 60_000;
* looking away from the planet.
* Heading and position are kept. Tracked cameras, locked ones (non-identity
* 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`
* 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.
@@ -202,16 +205,18 @@ export function installGlobeInViewRecovery(
if (!scene || !camera?.moveEnd || !camera?.moveStart)
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 = () => {
if (!isFreeGlobeCamera(viewer)) return;
if (!isFreeGlobeCamera(viewer)) return true;
// Cesium has no public running-flight flag.
if (camera._currentFlight || paused()) return;
if (camera._currentFlight || paused()) return false;
const height = camera.positionCartographic?.height;
if (
!(height > minHeight) ||
!globeOutOfFrame(camera, { keyholeRadius: keyholeRadius() })
)
return;
return true;
camera.setView({
orientation: {
heading: camera.heading,
@@ -220,6 +225,7 @@ export function installGlobeInViewRecovery(
},
});
scene.requestRender?.();
return true;
};
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
// for moveEnd.
if (restingSamples >= 2 && movedSinceCheck) {
if (restingSamples >= 2 && movedSinceCheck && recover())
movedSinceCheck = false;
recover();
}
if (settleElapsed >= SETTLE_GIVE_UP_MS) return;
Cesium.Cartesian3.clone(camera.positionWC, lastPosition);
Cesium.Cartesian3.clone(camera.directionWC, lastDirection);
@@ -274,8 +278,9 @@ export function installGlobeInViewRecovery(
settleTimer = schedule(sample, SETTLE_SAMPLE_MS);
};
const removeMoveEnd = camera.moveEnd.addEventListener(() => {
stopSettling();
recover();
if (recover()) stopSettling();
// Deferred: keep sampling so the check runs once the flight or scene ends.
else if (settleTimer === null) startSettling();
});
const removeMoveStart = camera.moveStart.addEventListener(startSettling);