fix(camera): hold the globe recovery while a pointer is down

Holding a drag still for half a second on a view that already showed no surface looked like a camera at rest, so the recovery turned the view down under the user's hand. The recovery now defers while a pointer pressed on the canvas is held, and runs once it is released, as for a flight or a playing scene.
This commit is contained in:
Sameh Khamis committed 2026-11-05 15:55:52 -08:00
1 parent a20076fc5e
commit 44106b2bfb
3 files changed
+61 -8

No files matched your search

+33
View File
@@ -778,6 +778,39 @@ test('a check a scene or flight defers runs once it ends', () => {
}
});
// Holding a drag still for half a second on a view that already showed no
// surface looked like a settled camera, and the view was turned down under
// the user's hand.
test('the recovery waits while a pointer is held on the globe', () => {
const timers = fakeTimers();
const { camera, scene, viewer } = realCamera({ width: 1225, height: 923 });
const document = new EventTarget();
scene.canvas = Object.assign(new EventTarget(), {
clientWidth: 1225,
clientHeight: 923,
ownerDocument: document,
});
const dispose = installGlobeInViewRecovery(viewer, timers);
aim(camera, reportedStuckView);
scene.canvas.dispatchEvent(new Event('pointerdown'));
camera.moveStart.raiseEvent();
for (let i = 0; i < 8; i += 1) timers.tick();
camera.moveEnd.raiseEvent();
for (let i = 0; i < 8; i += 1) timers.tick();
assert.equal(globeOutOfFrame(camera), true, 'left alone while held');
document.dispatchEvent(new Event('pointerup'));
for (let i = 0; i < 3; i += 1) timers.tick();
assert.ok(
Cesium.Math.equalsEpsilon(camera.pitch, -Math.PI / 2, 1e-6),
'recovered once the pointer is released',
);
dispose();
aim(camera, reportedStuckView);
scene.canvas.dispatchEvent(new Event('pointerdown'));
document.dispatchEvent(new Event('pointerup'));
assert.equal(timers.pending(), 0, 'disposed recovery holds no listeners');
});
function fakeTimers() {
let next = 1;
const pending = new Map();
+25 -6
View File
@@ -172,9 +172,10 @@ const SETTLE_GIVE_UP_MS = 5 * 60_000;
* catches a saved or shared view that looked away, or any other move that ends
* 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; a
* check they defer runs once they end, however long they last.
* transform), a camera flight in progress, a pointer held down on the canvas
* (a drag held still), the 2D and Columbus modes, and any time `paused`
* returns true (an authored scene playing) are left alone; a check they defer
* runs once they end, however long they last.
* `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.
@@ -205,12 +206,27 @@ 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.
// A pointer held on the globe is a gesture in progress, even when it holds
// still for half a second: the check waits for its release.
const canvas = scene.canvas;
const releaseTarget = canvas?.ownerDocument ?? canvas;
let pointerHeld = false;
const onPointerDown = () => {
pointerHeld = true;
};
const onPointerUp = () => {
pointerHeld = false;
};
canvas?.addEventListener?.('pointerdown', onPointerDown);
releaseTarget?.addEventListener?.('pointerup', onPointerUp);
releaseTarget?.addEventListener?.('pointercancel', onPointerUp);
// Returns false while a flight, a paused scene or a held pointer defers the
// check, so the caller can try again once it ends.
const recover = () => {
if (!isFreeGlobeCamera(viewer)) return true;
// Cesium has no public running-flight flag.
if (camera._currentFlight || paused()) return false;
if (camera._currentFlight || paused() || pointerHeld) return false;
const height = camera.positionCartographic?.height;
if (
!(height > minHeight) ||
@@ -294,6 +310,9 @@ export function installGlobeInViewRecovery(
stopSettling();
removeMoveEnd();
removeMoveStart();
canvas?.removeEventListener?.('pointerdown', onPointerDown);
releaseTarget?.removeEventListener?.('pointerup', onPointerUp);
releaseTarget?.removeEventListener?.('pointercancel', onPointerUp);
};
}