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

+3 -2
View File
@@ -103,8 +103,9 @@ zoom-out guard above keeps wheel and pinch zoom-outs in view; this catches the
other ways a move can end looking away, such as a saved or shared view that did.
Views with more surface than that, such as the oblique orbital Director shots,
are left as they are, and so are tracked or locked cameras, a camera flight in
progress, a Director scene while it plays (holds included) and the 2D and
Columbus modes; a check a flight or scene defers runs once it ends. A Director
progress, a Director scene while it plays (holds included), a pointer held
down on the globe (a drag held still) and the 2D and Columbus modes; a check a
flight, scene or held pointer defers runs once it ends. A Director
shot loaded or replayed outside a run is not a playing scene. A view that loses
its surface without a camera move (a resize, or turning the scope on) is left as
it is until the next move. A voice turn requested while the camera is tracked is
+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);
};
}