fix(awareness): keep a long drag or touch counting as camera input

Only canvas events refreshed the 1.5 s input window, and a touch only on
touchstart. Cesium follows a drag that leaves the canvas at document
level (in Firefox, and wherever there are no pointer events), so a long
gesture kept moving the camera after the window closed, and the pulses
rested mid-gesture. From a pointer or touch start on the canvas until
its end or cancel, moves at document level now count too; the listeners
go when the gesture ends or the layer detaches.
This commit is contained in:
Sameh Khamis committed 2026-11-03 10:40:30 -08:00
1 parent 56a7cacbec
commit de634952ec
3 files changed
+71 -1

No files matched your search

+31
View File
@@ -23,6 +23,36 @@ export function createLifecycle({
if (event?.type === 'pointermove' && !event.buttons) return;
if (!cameraInputsEnabled()) return;
layerState.lastCameraInputMs = Date.now();
if (event?.type === 'pointerdown' || event?.type === 'touchstart')
followGesture(true);
}
// A drag can leave the canvas, and Cesium follows it at document level
// (in Firefox, and wherever there are no pointer events). So does this,
// from a gesture's start on the canvas to its end, so a long gesture keeps
// the input window open the whole way.
const GESTURE_MOVE_EVENTS = ['pointermove', 'touchmove'];
const GESTURE_END_EVENTS = [
'pointerup',
'pointercancel',
'touchend',
'touchcancel',
];
let followingGesture = false;
function followGesture(on) {
if (followingGesture === on) return;
followingGesture = on;
const listen = on ? 'addEventListener' : 'removeEventListener';
for (const type of GESTURE_MOVE_EVENTS)
globalThis.document?.[listen]?.(type, noteCameraInput, { passive: true });
for (const type of GESTURE_END_EVENTS)
globalThis.document?.[listen]?.(type, endGesture, { passive: true });
}
function endGesture(event) {
noteCameraInput(event);
followGesture(false);
}
function cameraInputsEnabled() {
@@ -160,6 +190,7 @@ export function createLifecycle({
noteCameraInput,
);
layerState.cameraInputTarget = null;
followGesture(false);
}
layerState.preRenderRemover?.();
layerState.preRenderRemover = null;
+39
View File
@@ -667,6 +667,45 @@ test('input the camera controller ignores, as in the cockpit, does not wake the
assert.equal(pulsing(), true, 'a drag after the cockpit');
});
test('a long drag or touch keeps the pulses awake after it leaves the canvas', (t) => {
const ring = ringRendering(t);
const { inputWakeMs } = AWARENESS_RANGE_PULSE;
const { dispatch, pan, pulsing, loop, lifecycle } = inputLifecycle(t, ring);
const onDocument = (type, init = {}) =>
document.dispatchEvent(Object.assign(new Event(type), init));
// The gesture starts on the canvas and carries on past `inputWakeMs`
// outside it, where Cesium still follows it at document level.
const longGesture = (start, move, init) => {
dispatch(start, init);
for (let ms = 0; ms < inputWakeMs + 1000; ms += 250) {
loop.advance(250);
onDocument(move, init);
}
};
for (const [start, move, end, init] of [
['pointerdown', 'pointermove', 'pointerup', { buttons: 1 }],
['touchstart', 'touchmove', 'touchend', {}],
['touchstart', 'touchmove', 'touchcancel', {}],
]) {
longGesture(start, move, init);
pan();
assert.equal(pulsing(), true, `${move} outside the canvas`);
// Once it ends, moves elsewhere are not handling the map.
onDocument(end);
loop.advance(inputWakeMs + 100);
onDocument(move, init);
pan();
assert.equal(pulsing(), false, `${move} after ${end}`);
}
// Detaching mid-gesture drops the document listeners too.
dispatch('pointerdown');
lifecycle.detachRuntimeListeners();
loop.advance(inputWakeMs + 100);
const noted = ring.state.lastCameraInputMs;
onDocument('pointermove', { buttons: 1 });
assert.equal(ring.state.lastCameraInputMs, noted);
});
test('disabling the layer tears the pulses down completely', (t) => {
const ring = ringRendering(t);
const { state, scene, rendering, motion, pulseFrames } = ring;