diff --git a/src/layers/streetLevel/selection.js b/src/layers/streetLevel/selection.js index 14eeb59ea..c11be6619 100644 --- a/src/layers/streetLevel/selection.js +++ b/src/layers/streetLevel/selection.js @@ -51,10 +51,13 @@ export function createSelection({ state, parts }) { parts.sequences.clearSelection(); } - /** Where the pointer is, until a frame picks it; a drag picks nothing. */ - let hoverPosition = null; + /** The pointer over the globe, and whether a frame is queued to pick it. */ + let pointer = null; + let hoverQueued = false; let hoverCursor = false; - let dragging = false; + /** Buttons held: any of them, with any modifier, is a camera drag. */ + let pointerButtons = 0; + let removeHoverWatchers = null; function setHoverCursor(canvas, hit) { if (hit === hoverCursor) return; @@ -63,14 +66,14 @@ export function createSelection({ state, parts }) { } function pickHover() { - const position = hoverPosition; - hoverPosition = null; + hoverQueued = false; const canvas = state.viewer?.scene?.canvas; - // Switched off (or torn down) since the frame was queued. - if (!canvas || !state.enabled || !state.clickHandler || dragging) return; + // Switched off (or torn down) since the frame was queued, or dragging. + if (!canvas || !state.enabled || !state.clickHandler || pointerButtons) + return; let hit = false; try { - const picked = state.viewer.scene.pick(position); + const picked = state.viewer.scene.pick(pointer); hit = ownsPick( picking?.resolvePickId ? picking.resolvePickId(picked) : picked?.id, ); @@ -80,11 +83,33 @@ export function createSelection({ state, parts }) { setHoverCursor(canvas, hit); } - /** Pointer cursor over anything this layer owns, picked at most once a frame. */ + /** Pick the pointer on the next frame; at most one pick a frame. */ + function queueHoverPick() { + if (hoverQueued || !pointer || pointerButtons) return; + hoverQueued = true; + requestAnimationFrame(pickHover); + } + + /** Pointer cursor over anything this layer owns. */ function onMove(movement) { - if (!state.enabled || dragging) return; - if (!hoverPosition) requestAnimationFrame(pickHover); - hoverPosition = Cesium.Cartesian2.clone(movement.endPosition); + if (!state.enabled) return; + pointer = Cesium.Cartesian2.clone(movement.endPosition, pointer); + queueHoverPick(); + } + + /** Track held buttons, and pick again once the camera rests under a still pointer. */ + function watchHover(viewer) { + const canvas = viewer.scene.canvas; + const onButtons = (event) => { + pointerButtons = event.buttons ?? 0; + }; + const types = ['pointerdown', 'pointermove', 'pointerup']; + for (const type of types) canvas.addEventListener?.(type, onButtons); + const removeEnd = viewer.camera?.moveEnd?.addEventListener(queueHoverPick); + removeHoverWatchers = () => { + for (const type of types) canvas.removeEventListener?.(type, onButtons); + removeEnd?.(); + }; } function install(viewer) { @@ -100,12 +125,7 @@ export function createSelection({ state, parts }) { onMove, Cesium.ScreenSpaceEventType.MOUSE_MOVE, ); - state.clickHandler.setInputAction(() => { - dragging = true; - }, Cesium.ScreenSpaceEventType.LEFT_DOWN); - state.clickHandler.setInputAction(() => { - dragging = false; - }, Cesium.ScreenSpaceEventType.LEFT_UP); + watchHover(viewer); document.addEventListener('keydown', onKeyDown); picking?.registerPickOwner?.(STREET_LEVEL_LAYER_ID, ownsPick); } @@ -113,8 +133,10 @@ export function createSelection({ state, parts }) { function uninstall() { // Nothing to undo for a layer that was never enabled. if (!state.clickHandler) return; - hoverPosition = null; - dragging = false; + removeHoverWatchers?.(); + removeHoverWatchers = null; + pointer = null; + pointerButtons = 0; if (hoverCursor && state.viewer?.scene?.canvas) { state.viewer.scene.canvas.style.cursor = ''; hoverCursor = false; diff --git a/src/layers/streetLevel/selection.test.mjs b/src/layers/streetLevel/selection.test.mjs index 7eaedf4f5..be9bf304d 100644 --- a/src/layers/streetLevel/selection.test.mjs +++ b/src/layers/streetLevel/selection.test.mjs @@ -41,7 +41,9 @@ function harness({ selected = false } = {}) { disableRootEvents: true, onwheel: null, }); + const camera = { moveEnd: new Cesium.Event() }; const viewer = { + camera, scene: { canvas, pick(position) { @@ -97,8 +99,13 @@ function harness({ selected = false } = {}) { canvas, picks, move: (x) => onMove({ endPosition: { x, y: 10 } }), - down: () => action('LEFT_DOWN')(), - up: () => action('LEFT_UP')(), + camera, + /** A pointer event on the canvas with `buttons` held. */ + buttons(type, buttons) { + const event = new Event(type); + Object.defineProperty(event, 'buttons', { value: buttons }); + canvas.dispatchEvent(event); + }, frame() { for (const task of frames.splice(0)) task(); }, @@ -132,14 +139,46 @@ test('hover picks once a frame, at the latest position, and shows a pointer over } }); -test('a drag, or a layer gone off before the frame, picks nothing', () => { +test('a drag with any button picks nothing, and a release with a modifier held does not stick', () => { const h = harness(); try { - h.down(); - h.move(1); + for (const held of [1, 2, 4]) { + h.buttons('pointerdown', held); + h.move(held); + h.frame(); + } + assert.equal(h.picks.length, 0, 'left, right and middle drags'); + // Released with Shift held: Cesium's plain LEFT_UP never fires. + h.buttons('pointerup', 0); + h.move(5); h.frame(); - assert.equal(h.picks.length, 0, 'a drag in progress'); - h.up(); + assert.equal(h.picks.length, 1, 'hover picks again'); + } finally { + h.restore(); + } +}); + +test('when the camera rests, a still pointer is picked again so the cursor cannot stick', () => { + const h = harness(); + try { + h.scene.under = 'mly:seq:1'; + h.move(5); + h.frame(); + assert.equal(h.canvas.style.cursor, 'pointer'); + // The camera moves the line out from under a still pointer. + h.scene.under = null; + h.camera.moveEnd.raiseEvent(); + h.frame(); + assert.equal(h.picks.length, 2); + assert.equal(h.canvas.style.cursor, ''); + } finally { + h.restore(); + } +}); + +test('a hover frame queued before the layer went off does not pick', () => { + const h = harness(); + try { h.move(2); h.state.enabled = false; h.frame();