diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 9eb75f2a9..4ec5742db 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -14,6 +14,9 @@ "src/data/scenePick.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/input/sensorPick.js", "src/data/infrastructureOverlayEntry.js", "src/sources/infrastructureData.js", @@ -884,7 +887,10 @@ "src/data/pickRegistry.js", "src/input/sensorPick.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium"] }, @@ -1143,7 +1149,10 @@ "src/data/pickRegistry.js", "src/input/sensorPick.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium", "vue", "pinia", "vue-i18n"] }, @@ -1282,7 +1291,10 @@ "src/data/scenePick.js", "src/data/pickRegistry.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium", "vue", "pinia", "vue-i18n"] }, @@ -1376,6 +1388,9 @@ "src/data/pickRegistry.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/render/cesium/internals.js" ], "external": ["cesium"] @@ -1448,7 +1463,10 @@ "src/input/sensorPick.js", "src/data/pickRegistry.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium"] }, @@ -1533,7 +1551,10 @@ "src/data/pickRegistry.js", "src/input/sensorPick.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium"] }, @@ -1620,7 +1641,10 @@ "src/data/scenePick.js", "src/data/pickRegistry.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium"] }, @@ -1665,7 +1689,10 @@ "src/data/scenePick.js", "src/data/pickRegistry.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium", "satellite.js"] }, @@ -1710,7 +1737,10 @@ "src/data/pickRegistry.js", "src/input/sensorPick.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium"] }, @@ -1768,7 +1798,10 @@ "src/data/scenePick.js", "src/data/pickRegistry.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium", "@mapbox/vector-tile", "pbf"] }, @@ -1916,6 +1949,9 @@ "src/data/pickRegistry.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/queries/heights.js" ], "external": ["cesium"] @@ -1986,7 +2022,10 @@ "src/data/pickRegistry.js", "src/input/sensorPick.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["hls.js", "cesium"] }, @@ -2065,6 +2104,9 @@ "src/data/pickRegistry.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/queries/heights.js" ], "external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"] @@ -2110,6 +2152,9 @@ "src/data/pickRegistry.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/queries/heights.js" ], "external": ["cesium", "@mapbox/vector-tile", "pbf"] @@ -2164,6 +2209,9 @@ "src/data/pickRegistry.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/render/cesium/internals.js" ], "external": ["cesium"] @@ -2201,7 +2249,10 @@ "src/input/sensorPick.js", "src/data/pickRegistry.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium"] }, @@ -3363,6 +3414,8 @@ "src/input/sensorPick.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/input/router.js", "src/queries/heights.js", "src/render/cesium/internals.js", "src/render/cesium/tileRequests.js" @@ -3992,6 +4045,9 @@ "src/input/sensorPick.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/queries/heights.js", "src/render/cesium/tileRequests.js", "src/render/cesium/internals.js" @@ -4104,6 +4160,9 @@ "modules": [ "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/input/sensorPick.js", "src/data/inputOwnership.js", "src/data/pickRegistry.js", @@ -4362,7 +4421,10 @@ "src/input/sensorPick.js", "src/data/pickRegistry.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium"] }, @@ -4695,6 +4757,9 @@ "src/input/sensorPick.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/queries/heights.js", "src/render/cesium/internals.js" ], @@ -4754,6 +4819,9 @@ "src/data/pickRegistry.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/queries/heights.js", "src/render/cesium/internals.js" ], @@ -4822,7 +4890,10 @@ "src/data/scenePick.js", "src/data/pickRegistry.js", "src/input/dispatcher.js", - "src/render/cesium/cameraInput.js" + "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js" ], "external": ["cesium"] }, @@ -4924,6 +4995,9 @@ "src/data/pickRegistry.js", "src/input/dispatcher.js", "src/render/cesium/cameraInput.js", + "src/render/cesium/input.js", + "src/render/cesium/ctrlWheelZoom.js", + "src/render/input/router.js", "src/queries/heights.js" ], "external": ["cesium"] diff --git a/scripts/qa-draw-tool.mjs b/scripts/qa-draw-tool.mjs index d579234e9..d0c5f1f19 100644 --- a/scripts/qa-draw-tool.mjs +++ b/scripts/qa-draw-tool.mjs @@ -263,39 +263,16 @@ try { }), ); - // The viewer's OWN click actions, captured before the tool ever runs. Both - // must be absent while drawing and back — as the SAME functions — after. - await page.evaluate(() => { - const Cesium = window.__CESIUM__; - const stock = window.__godsEyeView.viewer.screenSpaceEventHandler; - window.__qaStock = { - click: - stock.getInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK) || null, - double: - stock.getInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) || - null, + // The viewer's own select and follow, which the input port runs: on before + // the tool runs, off while drawing, and on again after. + const stockActions = async () => { + const state = await drawState(); + return { + clickOn: state.stockSingleClick, + doubleOn: state.stockDoubleClick, }; - }); - const stockActions = () => - page.evaluate(() => { - const Cesium = window.__CESIUM__; - const stock = window.__godsEyeView.viewer.screenSpaceEventHandler; - const live = { - click: - stock.getInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK) || null, - double: - stock.getInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) || - null, - }; - return { - clickPresent: Boolean(live.click), - doublePresent: Boolean(live.double), - clickIsOriginal: live.click === window.__qaStock.click, - doubleIsOriginal: live.double === window.__qaStock.double, - hadClick: Boolean(window.__qaStock.click), - hadDouble: Boolean(window.__qaStock.double), - }; - }); + }; + const beforeDraw = await stockActions(); // ── enable / disable cycles leave nothing behind ──────────────────────── const before = await drawState(); @@ -318,24 +295,21 @@ try { `handler ${after.sceneHandler}, owner ${after.pointerOwner}, stock dblclick ${after.stockDoubleClick}`, ); - // ── the viewer's own click actions are borrowed, then given back ──────── + // ── the viewer's own click actions are held off, then given back ─────── await page.click('#draw-toggle'); const duringDraw = await stockActions(); check( - "Cesium's own selecting click and tracking double-click are both borrowed while drawing", - duringDraw.clickPresent === false && duringDraw.doublePresent === false, - `click present ${duringDraw.clickPresent}, double present ${duringDraw.doublePresent} ` + - `(viewer had click ${duringDraw.hadClick}, double ${duringDraw.hadDouble})`, + "Cesium's own selecting click and tracking double-click are both off while drawing", + duringDraw.clickOn === false && duringDraw.doubleOn === false, + JSON.stringify({ beforeDraw, duringDraw }), ); await page.click('#draw-toggle'); const afterDraw = await stockActions(); check( - 'and both are restored as the SAME functions, not replacements', - afterDraw.clickPresent === duringDraw.hadClick && - afterDraw.doublePresent === duringDraw.hadDouble && - afterDraw.clickIsOriginal && - afterDraw.doubleIsOriginal, - JSON.stringify(afterDraw), + 'and both are back after', + afterDraw.clickOn === beforeDraw.clickOn && + afterDraw.doubleOn === beforeDraw.doubleOn, + JSON.stringify({ beforeDraw, afterDraw }), ); // ── a pin, finished with Enter ────────────────────────────────────────── @@ -664,19 +638,6 @@ try { document.getElementById('draw-toggle')?.getBoundingClientRect().width > 0, { timeout: 10_000 }, ); - // The reload built a new page context: re-capture the viewer's own click - // actions so the identity comparisons below still mean something. - await page.evaluate(() => { - const Cesium = window.__CESIUM__; - const stock = window.__godsEyeView.viewer.screenSpaceEventHandler; - window.__qaStock = { - click: - stock.getInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK) || null, - double: - stock.getInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) || - null, - }; - }); await page.evaluate((view) => { const viewer = window.__godsEyeView.viewer; viewer.camera.cancelFlight?.(); @@ -827,24 +788,20 @@ try { const wasActive = tool.active; await tool.destroy(); await new Promise((resolve) => setTimeout(resolve, 50)); - const Cesium = window.__CESIUM__; const viewer = window.__godsEyeView.viewer; - const stock = viewer.screenSpaceEventHandler; let previews = 0; for (let i = 0; i < viewer.dataSources.length; i += 1) if (viewer.dataSources.get(i)?.name === 'gev-draw-preview') previews += 1; + const diagnostics = tool.diagnostics(); return { wasActive, - diagnostics: tool.diagnostics(), + diagnostics, previews, dataSources: viewer.dataSources.length, handleGone: window.__gevDrawTool === undefined, - clickRestored: - stock.getInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK) === - window.__qaStock.click, - doubleRestored: - stock.getInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) === - window.__qaStock.double, + // The viewer's own select and follow run again. + clickRestored: diagnostics.stockSingleClick, + doubleRestored: diagnostics.stockDoubleClick, drawingClass: document.body.classList.contains('gev-drawing'), }; }); diff --git a/src/annotations/drawTool.js b/src/annotations/drawTool.js index 905901f47..827e99e62 100644 --- a/src/annotations/drawTool.js +++ b/src/annotations/drawTool.js @@ -509,21 +509,10 @@ export function initDrawTool({ previewDataSources: countPreviewDataSources(viewer), previewEntities: destroyed ? 0 : dataSource.entities.values.length, pointerOwner: pointerOwner(), - // The viewer's OWN click actions. While a session is open both are - // borrowed (absent here); after it closes both are back. A harness can - // compare the restored functions by identity with what it captured - // before, which is the only way to prove they were given back rather - // than replaced. - stockSingleClick: Boolean( - viewer.screenSpaceEventHandler?.getInputAction?.( - Cesium.ScreenSpaceEventType.LEFT_CLICK, - ), - ), - stockDoubleClick: Boolean( - viewer.screenSpaceEventHandler?.getInputAction?.( - Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK, - ), - ), + // The viewer's own select and follow, which the input port runs: off + // while a session is open, back on after it closes. + stockSingleClick: inputFor(viewer).stockEnabled('select'), + stockDoubleClick: inputFor(viewer).stockEnabled('dblclickFollow'), }; }, /** Resolves once every deferred teardown step has run. */ diff --git a/src/annotations/drawToolBehaviour.test.mjs b/src/annotations/drawToolBehaviour.test.mjs index b76181db2..a99d85569 100644 --- a/src/annotations/drawToolBehaviour.test.mjs +++ b/src/annotations/drawToolBehaviour.test.mjs @@ -224,11 +224,13 @@ function fakeAnnotations() { } /** Stand up a tool against fresh doubles, with globals restored afterwards. */ -function harness({ navigation = null } = {}) { +function harness({ navigation = null, beforeInput = () => {} } = {}) { resetPointerOwnership(); const { byId, documentDouble } = fakeDom(); const viewer = fakeViewer(); - const input = fakeInput(viewer, { eventTypes: Cesium.ScreenSpaceEventType }); + // The viewer's own actions are bound before its input port takes them. + beforeInput(viewer); + const input = fakeInput(viewer); const annotations = fakeAnnotations(); const previousDocument = globalThis.document; const previousWindow = globalThis.window; @@ -339,30 +341,33 @@ test('a destroyed instance cannot free the pointer its replacement holds', async } }); -test('both stock viewer click actions are borrowed for the session and given back', async () => { - const h = harness(); +test('both stock viewer click actions are held off for the session and given back', async () => { + const stockClick = () => 'viewer picks and selects'; + const stockDouble = () => 'viewer tracks'; + const h = harness({ + beforeInput(viewer) { + viewer.screenSpaceEventHandler.setInputAction(stockClick, LEFT_CLICK); + viewer.screenSpaceEventHandler.setInputAction(stockDouble, LEFT_DOUBLE_CLICK); + }, + }); try { - const stockClick = () => 'viewer picks and selects'; - const stockDouble = () => 'viewer tracks'; - h.viewer.screenSpaceEventHandler.setInputAction(stockClick, LEFT_CLICK); - h.viewer.screenSpaceEventHandler.setInputAction(stockDouble, LEFT_DOUBLE_CLICK); - + assert.equal(h.input.stockEnabled('select'), true); h.act('toggleDraw'); assert.equal( - h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK), - undefined, + h.input.stockEnabled('select'), + false, "the viewer's own selecting click must not run while drawing", ); - assert.equal( - h.viewer.screenSpaceEventHandler.getInputAction(LEFT_DOUBLE_CLICK), - undefined, - ); + assert.equal(h.input.stockEnabled('dblclickFollow'), false); h.act('toggleDraw'); + assert.equal(h.input.stockEnabled('select'), true); + assert.equal(h.input.stockEnabled('dblclickFollow'), true); + h.input.destroy(); assert.equal( h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK), stockClick, - 'the exact function is restored, not a replacement', + 'the exact function goes back to the viewer, not a replacement', ); assert.equal( h.viewer.screenSpaceEventHandler.getInputAction(LEFT_DOUBLE_CLICK), diff --git a/src/app/scene.js b/src/app/scene.js index b1405108b..f50c543ae 100644 --- a/src/app/scene.js +++ b/src/app/scene.js @@ -1,6 +1,5 @@ import { createApplicationOperations } from './operations.js'; import * as Cesium from 'cesium'; -import { installTrackpadPinchZoom } from '../render/cesium/ctrlWheelZoom.js'; import { cesiumRenderer, cesiumViewerOf, @@ -87,7 +86,8 @@ export async function createApplicationScene({ // Deferred after the viewer, so it is released before the viewer goes. const input = inputFor(viewer); defer(() => input.destroy()); - defer(installTrackpadPinchZoom(viewer)); + // Ctrl+wheel and trackpad pinches zoom the globe, not the page. + renderer.input.ctrlWheelZoom = true; registerDataCredits(viewer, credits); configureCreditKeyboardAccess(document); defer(onLocaleChange(() => configureCreditKeyboardAccess(document))); diff --git a/src/data/alprCameras.test.mjs b/src/data/alprCameras.test.mjs index a09dd6887..9f381db96 100644 --- a/src/data/alprCameras.test.mjs +++ b/src/data/alprCameras.test.mjs @@ -348,7 +348,12 @@ function cameraHarness(layer = alprCamerasLayer) { type, modifier, ) { - if (type === Cesium.ScreenSpaceEventType.LEFT_CLICK) click = fn; + // The unmodified click: the input port binds one per modifier. + if ( + type === Cesium.ScreenSpaceEventType.LEFT_CLICK && + modifier === undefined + ) + click = fn; return setInput.call(this, fn, type, modifier); }; try { diff --git a/src/data/infrastructure.test.mjs b/src/data/infrastructure.test.mjs index 21f93b9a4..05ea476da 100644 --- a/src/data/infrastructure.test.mjs +++ b/src/data/infrastructure.test.mjs @@ -187,15 +187,18 @@ test('consumer build includes only infrastructure code and resolves assets under const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js')); assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [ 'cameraInput.js', + 'ctrlWheelZoom.js', 'dispatcher.js', 'heights.js', 'infrastructure.js', 'infrastructureData.js', 'infrastructureOverlayEntry.js', + 'input.js', 'inputOwnership.js', 'localGeojsonCore.js', 'localGeojsonLod.js', 'pickRegistry.js', + 'router.js', 'scenePick.js', 'sensorPick.js', ]); diff --git a/src/data/militaryInstallations.test.mjs b/src/data/militaryInstallations.test.mjs index 1b4065a0b..89ae37cc8 100644 --- a/src/data/militaryInstallations.test.mjs +++ b/src/data/militaryInstallations.test.mjs @@ -345,7 +345,8 @@ async function runInstallationLoad({ const originalSetInputAction = Cesium.ScreenSpaceEventHandler.prototype.setInputAction; Cesium.ScreenSpaceEventHandler.prototype.setInputAction = function (action, type, modifier) { - if (type === Cesium.ScreenSpaceEventType.LEFT_CLICK) clickAction = action; + // The unmodified click: the input port binds one per modifier. + if (type === Cesium.ScreenSpaceEventType.LEFT_CLICK && modifier === undefined) clickAction = action; return originalSetInputAction.call(this, action, type, modifier); }; try { militaryInstallationsLayer.init(viewer); } diff --git a/src/input/dispatcher.js b/src/input/dispatcher.js index 0c3c2df82..3db77e559 100644 --- a/src/input/dispatcher.js +++ b/src/input/dispatcher.js @@ -1,8 +1,11 @@ /** * @module input/dispatcher - * @description The one place pointer input over the globe comes in: a single - * `ScreenSpaceEventHandler` per scene, a click picked once and handed to - * every subscriber, and the camera-input and stock-click holds tools take. + * @description The one place the application's layers and tools take + * pointer input over the globe: the scene's input port + * (src/render/cesium/input.js), a click picked once and handed to every + * subscriber, and the camera-input and stock-click holds tools take. Each + * layer family moves onto the renderer's own `input` port as it is ported; + * until then it subscribes here, with the events and shapes it always had. * * Layers used to bind a handler each and pick the same click again, so one * click with the usual layers on cost about 25 `scene.pick` passes (each one @@ -33,6 +36,7 @@ import * as Cesium from 'cesium'; import { isLeaseCurrent, isPointerFree } from '../data/inputOwnership.js'; import { acquireCameraInput } from '../render/cesium/cameraInput.js'; +import { cesiumInputOf, createCesiumInput } from '../render/cesium/input.js'; import { findPickOwner, pickContact, @@ -50,12 +54,23 @@ export { releasePointer, } from '../data/inputOwnership.js'; +/** The handler event types the scene's input port binds, for test doubles. */ +export const HANDLER_EVENT_TYPES = Cesium.ScreenSpaceEventType; + /** A drill is run at least this deep and sliced, so callers asking for * different depths on one click share one pass. */ const DRILL_DEPTH = 16; const POINTER_TYPES = ['down', 'move', 'up', 'click', 'dblclick']; +/** + * The events Cesium's unmodified left-button actions raised, which is all + * the dispatcher's subscribers have ever seen: the port also has the other + * buttons and the modified presses. + */ +const unmodified = ({ modifiers: m }) => !m.shift && !m.ctrl && !m.alt; +const unmodifiedLeft = (ev) => ev.button === 0 && unmodified(ev); + /** A tool that re-arms takes a new lease each time: it may pass a getter. */ const leaseOf = (entry) => typeof entry.lease === 'function' ? entry.lease() : entry.lease; @@ -106,6 +121,9 @@ const dispatchers = new WeakMap(); * off; returns this hold's release. * @property {() => () => void} suppressStockClicks Take the viewer's stock * select and double-click-track actions off; returns this hold's release. + * @property {(name: 'select'|'dblclickFollow') => boolean} stockEnabled + * Whether the viewer's stock select or follow runs now: it does unless a + * hold is out (or the viewer has none). * @property {() => number} subscriberCount Live click and pointer * subscriptions, for leak checks. * @property {() => void} destroy @@ -114,8 +132,9 @@ const dispatchers = new WeakMap(); /** * The scene's dispatcher, made on first use. * @param {object} viewer A Cesium viewer (or a test double with `scene`). - * @param {{createHandler?: (canvas: object) => object, eventTypes?: object, now?: () => number}} [options] - * Used only by the call that makes it. + * @param {{createHandler?: (canvas: object) => object, now?: () => number, document?: object|null}} [options] + * For the input port it makes when the scene has none (tests pass a fake + * handler); used only by the call that makes it. * @returns {InputDispatcher} */ export function inputFor(viewer, options = {}) { @@ -128,16 +147,23 @@ export function inputFor(viewer, options = {}) { return dispatcher; } -function createDispatcher( - viewer, - { - createHandler = (canvas) => new Cesium.ScreenSpaceEventHandler(canvas), - eventTypes = Cesium.ScreenSpaceEventType, - now = () => performance.now(), - }, -) { +function createDispatcher(viewer, { createHandler, now, document } = {}) { const scene = viewer.scene; - const handler = createHandler(scene.canvas); + // The renderer's port when the application made one; a viewer of the + // caller's own gets one for as long as this dispatcher lives. + const shared = cesiumInputOf(scene); + const cesiumInput = + shared ?? + createCesiumInput(viewer, { + report: (error) => console.error('Pointer input failed', error), + // The viewer is its owner's: its stock actions stay on its handler. + takeStock: false, + ...(createHandler ? { createHandler } : {}), + ...(now ? { now } : {}), + ...(document !== undefined ? { document } : {}), + }); + const { port } = cesiumInput; + const cameraInput = acquireCameraInput(scene); /** @type {Array<{fn: Function}>} */ const clickSubscribers = []; /** @type {Record>} */ @@ -145,42 +171,8 @@ function createDispatcher( POINTER_TYPES.map((type) => [type, []]), ); const stockHolds = new Set(); - let stockSaved = null; let destroyed = false; - let pressActive = false; - let pressStartedAt = 0; - let previousPosition = null; - let travelPx = 0; - let completedGesture = null; - - const appendTravel = (position) => { - if ( - !pressActive || - !Number.isFinite(position?.x) || - !Number.isFinite(position?.y) - ) - return; - if (previousPosition) { - travelPx += Math.hypot( - position.x - previousPosition.x, - position.y - previousPosition.y, - ); - } - previousPosition = { x: position.x, y: position.y }; - }; - - const finishPress = (position) => { - if (!pressActive) return; - appendTravel(position); - completedGesture = { - travelPx, - durationMs: Math.max(0, now() - pressStartedAt), - }; - pressActive = false; - previousPosition = null; - }; - const run = (list, free, arg) => { for (const entry of [...list]) { if (entry.lease ? !isLeaseCurrent(leaseOf(entry)) : !free) continue; @@ -192,43 +184,45 @@ function createDispatcher( } }; - handler.setInputAction((event) => { - pressActive = true; - pressStartedAt = now(); - previousPosition = null; - travelPx = 0; - completedGesture = null; - appendTravel(event?.position); - run(pointerSubscribers.down, isPointerFree(), event); - }, eventTypes.LEFT_DOWN); - - handler.setInputAction((event) => { - appendTravel(event?.endPosition ?? event?.position); - run(pointerSubscribers.move, true, event); - }, eventTypes.MOUSE_MOVE); - - handler.setInputAction((event) => { - finishPress(event?.position); - run(pointerSubscribers.up, true, event); - }, eventTypes.LEFT_UP); - - handler.setInputAction((click) => { - if (pressActive) finishPress(click?.position); - const gesture = completedGesture || { travelPx: 0, durationMs: 0 }; - completedGesture = null; - const free = isPointerFree(); - const event = createClickEvent(viewer, click, gesture); - run(pointerSubscribers.click, free, event); - if (free) run(clickSubscribers, true, event); - }, eventTypes.LEFT_CLICK); - - handler.setInputAction((event) => { - run(pointerSubscribers.dblclick, isPointerFree(), event); - }, eventTypes.LEFT_DOUBLE_CLICK); - - // Once the handler is bound, so a setup that throws holds no share of the - // scene's camera input. - const cameraInput = acquireCameraInput(scene); + const cartesian = (p) => new Cesium.Cartesian2(p.x, p.y); + // Each type is one port listener; the pointer's owner is read once per + // event, before any subscriber runs. + const offPort = [ + port.on('down', (ev) => { + if (!unmodifiedLeft(ev)) return; + run(pointerSubscribers.down, isPointerFree(), { + position: cartesian(ev.position), + }); + }), + port.on('move', (ev) => { + if (!unmodified(ev)) return; + run(pointerSubscribers.move, true, { + startPosition: cartesian(ev.position), + endPosition: cartesian(ev.endPosition ?? ev.position), + }); + }), + port.on('up', (ev) => { + if (!unmodifiedLeft(ev)) return; + run(pointerSubscribers.up, true, { position: cartesian(ev.position) }); + }), + port.on('click', (ev) => { + if (!unmodifiedLeft(ev)) return; + const free = isPointerFree(); + const event = createClickEvent( + viewer, + { position: cartesian(ev.position) }, + ev.gesture ?? { travelPx: 0, durationMs: 0 }, + ); + run(pointerSubscribers.click, free, event); + if (free) run(clickSubscribers, true, event); + }), + port.on('dblclick', (ev) => { + if (!unmodifiedLeft(ev)) return; + run(pointerSubscribers.dblclick, isPointerFree(), { + position: cartesian(ev.position), + }); + }), + ]; const subscribe = (list, entry) => { if (destroyed) return () => {}; @@ -239,22 +233,6 @@ function createDispatcher( }; }; - const stockHandler = () => viewer.screenSpaceEventHandler; - const stockTypes = () => [ - eventTypes.LEFT_CLICK, - eventTypes.LEFT_DOUBLE_CLICK, - ]; - - const restoreStock = () => { - const stock = stockHandler(); - if (stockSaved && stock && !stock.isDestroyed?.()) { - for (const [type, action] of stockSaved) { - if (action) stock.setInputAction(action, type); - } - } - stockSaved = null; - }; - return { onClick(fn) { return subscribe(clickSubscribers, { fn, lease: null }); @@ -273,26 +251,22 @@ function createDispatcher( suppressStockClicks() { if (destroyed) return () => {}; - const hold = {}; - if (stockHolds.size === 0) { - const stock = stockHandler(); - if (stock) { - // Cesium's own select and double-click-track run on the viewer's - // handler and never see the pointer lease. - stockSaved = stockTypes().map((type) => [ - type, - stock.getInputAction(type), - ]); - for (const type of stockTypes()) stock.removeInputAction(type); - } - } - stockHolds.add(hold); - return () => { - if (!stockHolds.delete(hold) || stockHolds.size > 0) return; - restoreStock(); + // Cesium's own select and double-click-track never see the pointer + // lease; the port runs them, and holds them off here. + const releases = [ + port.behaviours.select.suppress(), + port.behaviours.dblclickFollow.suppress(), + ]; + const hold = () => { + if (!stockHolds.delete(hold)) return; + for (const release of releases) release(); }; + stockHolds.add(hold); + return hold; }, + stockEnabled: (name) => port.behaviours[name]?.enabled ?? false, + subscriberCount() { return POINTER_TYPES.reduce( (sum, type) => sum + pointerSubscribers[type].length, @@ -306,12 +280,10 @@ function createDispatcher( dispatchers.delete(scene); clickSubscribers.length = 0; for (const type of POINTER_TYPES) pointerSubscribers[type].length = 0; + for (const off of offPort) off(); + for (const hold of [...stockHolds]) hold(); cameraInput.release(); - if (stockHolds.size) { - stockHolds.clear(); - restoreStock(); - } - if (!handler.isDestroyed?.()) handler.destroy?.(); + if (!shared) cesiumInput.dispose(); }, }; } diff --git a/src/input/testDoubles.mjs b/src/input/testDoubles.mjs index 106ecaf50..78baca3fd 100644 --- a/src/input/testDoubles.mjs +++ b/src/input/testDoubles.mjs @@ -3,30 +3,30 @@ * `ScreenSpaceEventHandler`, and a dispatcher made on it ahead of the code * under test, so that code's `inputFor(viewer)` gets the fake. */ -import { inputFor } from './dispatcher.js'; +import { HANDLER_EVENT_TYPES, inputFor } from './dispatcher.js'; -export const FAKE_EVENT_TYPES = Object.freeze({ - LEFT_DOWN: 'down', - MOUSE_MOVE: 'move', - LEFT_UP: 'up', - LEFT_CLICK: 'click', - LEFT_DOUBLE_CLICK: 'dblclick', -}); +/** Cesium's own event types, the ones the input port binds. */ +export const FAKE_EVENT_TYPES = HANDLER_EVENT_TYPES; export class FakeScreenSpaceEventHandler { constructor() { this.actions = new Map(); this.destroyed = false; } - setInputAction(fn, type) { - this.actions.set(type, fn); + /** One action per type and modifier, as Cesium keeps them. */ + static key(type, modifier) { + return modifier === undefined ? type : `${type}+${modifier}`; } - getInputAction(type) { - return this.actions.get(type); + setInputAction(fn, type, modifier) { + this.actions.set(FakeScreenSpaceEventHandler.key(type, modifier), fn); } - removeInputAction(type) { - this.actions.delete(type); + getInputAction(type, modifier) { + return this.actions.get(FakeScreenSpaceEventHandler.key(type, modifier)); } + removeInputAction(type, modifier) { + this.actions.delete(FakeScreenSpaceEventHandler.key(type, modifier)); + } + /** Raise an action as Cesium does for a press with no modifier held. */ fire(type, event) { this.actions.get(type)?.(event); } @@ -41,18 +41,16 @@ export class FakeScreenSpaceEventHandler { /** * Make `viewer`'s dispatcher on a fake handler and return ways to drive it. * @param {object} viewer - * @param {{now?: () => number, eventTypes?: object}} [options] `eventTypes` - * defaults to {@link FAKE_EVENT_TYPES}; pass Cesium's own when the code under - * test reads the viewer's stock actions by Cesium's type values. + * @param {{now?: () => number}} [options] */ -export function fakeInput(viewer, { now, eventTypes = FAKE_EVENT_TYPES } = {}) { +export function fakeInput(viewer, { now } = {}) { const handler = new FakeScreenSpaceEventHandler(); const input = inputFor(viewer, { createHandler: () => handler, - eventTypes, + document: null, ...(now ? { now } : {}), }); - const T = eventTypes; + const T = FAKE_EVENT_TYPES; return { input, handler, @@ -71,6 +69,8 @@ export function fakeInput(viewer, { now, eventTypes = FAKE_EVENT_TYPES } = {}) { }, dblclick: (position = { x: 10, y: 10 }) => handler.fire(T.LEFT_DOUBLE_CLICK, { position }), + /** Whether the viewer's stock `select` or `dblclickFollow` runs now. */ + stockEnabled: (name) => input.stockEnabled(name), destroy: () => input.destroy(), }; } diff --git a/src/layers/recentImagery/testDoubles.mjs b/src/layers/recentImagery/testDoubles.mjs index 7d4b338be..6186b51a9 100644 --- a/src/layers/recentImagery/testDoubles.mjs +++ b/src/layers/recentImagery/testDoubles.mjs @@ -133,7 +133,7 @@ export function boxToolFakes() { }, }; - const input = fakeInput(viewer, { eventTypes: EVENT_TYPES }); + const input = fakeInput(viewer); const pickWorld = (_viewer, nx, ny) => ny * 500 < 40 ? null : { lon: -97.9 + nx * 0.2, lat: 30.4 - ny * 0.2 }; @@ -162,6 +162,7 @@ export function boxToolFakes() { documentRef, pickWorld, input: input.input, + stockEnabled: input.stockEnabled, bound, fire, key, diff --git a/src/render/cesium/appRenderer.js b/src/render/cesium/appRenderer.js index 17d164ec9..c99019ca6 100644 --- a/src/render/cesium/appRenderer.js +++ b/src/render/cesium/appRenderer.js @@ -18,13 +18,15 @@ */ import { createApplicationViewer } from '../../app/viewer.js'; import { CONTRACT_VERSION } from '../contract/index.js'; +import { createCesiumInput } from './input.js'; import { cesiumCapabilities, createCesiumPorts } from './ports.js'; /** * The members a transitional renderer has today. * @typedef {Pick} + * | 'camera' | 'query' | 'input' | 'capture' | 'on' | 'resize' + * | 'destroy'>} * TransitionalRenderer */ @@ -83,12 +85,18 @@ export function createCesiumRendererPlugin({ document.body.appendChild(creditContainer); let viewer; let ports; + let input; let capabilities; try { viewer = createViewer({ container, creditContainer }); capabilities = new ReadonlySet(cesiumCapabilities(viewer.scene)); ports = createCesiumPorts(viewer, { capabilities }); + input = createCesiumInput(viewer, { + query: ports.query, + report: ports.report, + }); } catch (error) { + ports?.dispose(); if (viewer && !viewer.isDestroyed()) viewer.destroy(); creditContainer.remove(); throw error; @@ -102,6 +110,7 @@ export function createCesiumRendererPlugin({ draw: ports.draw, camera: ports.camera, query: ports.query, + input: input.port, capture: ports.capture, on: ports.on, resize() { @@ -109,6 +118,7 @@ export function createCesiumRendererPlugin({ }, destroy() { try { + input.dispose(); ports.dispose(); if (!viewer.isDestroyed()) viewer.destroy(); } finally { diff --git a/src/render/cesium/appRenderer.test.mjs b/src/render/cesium/appRenderer.test.mjs index f6da249cb..b502ff947 100644 --- a/src/render/cesium/appRenderer.test.mjs +++ b/src/render/cesium/appRenderer.test.mjs @@ -27,6 +27,9 @@ function fakeDocument() { createElement: make, body: { appendChild: (el) => children.push(el) }, getElementById: () => null, + // The input port watches the document's pointer events. + addEventListener() {}, + removeEventListener() {}, }; } @@ -53,7 +56,12 @@ function fakeViewer(log, { failDestroy = false } = {}) { preRender: fakeEvent(listeners), postRender: fakeEvent(listeners), }, - canvas: { tag: 'canvas' }, + canvas: { + tag: 'canvas', + // Cesium's input handler binds the canvas's events. + addEventListener() {}, + removeEventListener() {}, + }, resize: () => log.push('resize'), isDestroyed: () => destroyed, destroy() { @@ -346,7 +354,6 @@ const EXPECTED_MISSING = [ 'renderer.credits', 'renderer.effects', 'renderer.globe', - 'renderer.input', 'renderer.quality', 'renderer.readPixels', 'renderer.releaseUnused', diff --git a/src/render/cesium/input.js b/src/render/cesium/input.js index dcaf265c1..5daf19737 100644 --- a/src/render/cesium/input.js +++ b/src/render/cesium/input.js @@ -91,17 +91,33 @@ function pointerTypeOf(dom, otherwise = 'mouse') { return otherwise; } +/** @type {WeakMap} scene -> its input port */ +const inputs = new WeakMap(); + /** + * The input port made for a scene, while it lives: the application's + * dispatcher (src/input/dispatcher.js) delivers through it. + * @param {object} scene + * @returns {ReturnType | null} + */ +export const cesiumInputOf = (scene) => inputs.get(scene) ?? null; + +/** + * One per scene: a second would find the stock actions already taken. * @param {object} widget A Cesium `CesiumWidget` or `Viewer`. * @param {object} options - * @param {object} options.query The renderer's query port: clicks pick - * through it. + * @param {object} [options.query] The renderer's query port: clicks pick + * through it. Without one, clicks carry no picks, for a caller that makes + * its own (the application's dispatcher, over a viewer of its own). * @param {(error: unknown) => void} options.report Where a listener's or a * `grab`'s error goes. * @param {(canvas: object) => object} [options.createHandler] * @param {() => number} [options.now] Event time, ms. * @param {object} [options.document] Where the DOM events are watched. * @param {typeof installTrackpadPinchZoom} [options.ctrlWheelZoom] + * @param {boolean} [options.takeStock] False leaves the viewer's stock + * actions on its own handler, taken off only while suppressed, for a + * viewer that is not this renderer's: its owner may still rebind them. * @returns {{port: object, feed: Function, listenerCount: () => number, dispose: () => void}} * `feed(type, x, y, extra)` delivers one event as the handler would, for * the conformance harness. @@ -115,31 +131,52 @@ export function createCesiumInput( now = () => performance.now(), document: dom = globalThis.document, ctrlWheelZoom = installTrackpadPinchZoom, + takeStock = true, }, ) { const scene = widget.scene; + if (inputs.has(scene)) + throw new TypeError('This scene already has an input port'); const canvas = scene.canvas; let disposed = false; - // The viewer's stock actions, now this port's to run. + // The viewer's stock actions, now this port's to run; or, not taking + // them, borrowed from its handler while suppressed. const stockHandler = widget.screenSpaceEventHandler; const stock = {}; + const has = {}; for (const [name, type] of Object.entries(STOCK)) { const action = stockHandler?.getInputAction?.(type); - if (!action) continue; + has[name] = Boolean(action); + if (!action || !takeStock) continue; stock[name] = action; stockHandler.removeInputAction(type); } + /** Actions borrowed while suppressed, to give back by identity. */ + const borrowed = {}; + const giveBack = (name) => { + const action = borrowed[name]; + delete borrowed[name]; + if (!action || !stockHandler || stockHandler.isDestroyed?.()) return; + if (!stockHandler.getInputAction(STOCK[name])) + stockHandler.setInputAction(action, STOCK[name]); + }; + function borrowStock(name, enabled) { + if (takeStock || !stockHandler) return; + if (enabled) return giveBack(name); + const action = stockHandler.getInputAction?.(STOCK[name]); + if (!action) return; + borrowed[name] = action; + stockHandler.removeInputAction(STOCK[name]); + } const cameraInput = acquireCameraInput(scene); let uninstallCtrlWheel = null; const router = createInputRouter({ report, suspendCamera: (reason) => cameraInput.suspend(reason), - supported: { - select: Boolean(stock.select), - dblclickFollow: Boolean(stock.dblclickFollow), - }, + supported: { select: has.select, dblclickFollow: has.dblclickFollow }, + onBehaviour: borrowStock, onCtrlWheelZoom(on) { if (disposed) return; if (on && !uninstallCtrlWheel) uninstallCtrlWheel = ctrlWheelZoom(widget); @@ -179,6 +216,13 @@ export function createCesiumInput( const finished = new Map(); /** The button of the last event, when it was an up. */ let lastUp = null; + /** The type of the last event delivered. */ + let lastType = null; + /** + * The stock behaviours as they stood at the last left down: a press that + * began suppressed stays so, even when its tool lets go on the up. + */ + let stockAtDown = null; const heldButton = () => { for (const button of [0, 1, 2]) if (pressing.has(button)) return button; return 0; @@ -277,6 +321,18 @@ export function createCesiumInput( track(type, at, end ?? at, button); const justUp = lastUp; lastUp = type === 'up' ? button : null; + const previous = lastType; + lastType = type; + if (type === 'down' && button === 0) + stockAtDown = { + select: behaviours.select.enabled, + dblclickFollow: behaviours.dblclickFollow.enabled, + }; + // A click straight after its up, and a double-click straight after + // that click, belong to the last press. + const ofPress = + (type === 'click' && justUp === 0) || + (type === 'dblclick' && previous === 'click'); const ev = { type, position: at, @@ -300,15 +356,22 @@ export function createCesiumInput( : undefined; finished.delete(button); ev.gesture = extra.gesture ?? press ?? { travelPx: 0, durationMs: 0 }; - withPicks(ev); + if (query) withPicks(ev); } // The viewer's own select and follow run first, as they did on its // handler. - router.dispatch(ev, (to) => runStock(ev, to)); + router.dispatch(ev, (to) => runStock(ev, to, ofPress ? stockAtDown : null)); + // A hold a down subscriber takes covers its press too. + if (type === 'down' && button === 0 && stockAtDown) + stockAtDown = { + select: stockAtDown.select && behaviours.select.enabled, + dblclickFollow: + stockAtDown.dblclickFollow && behaviours.dblclickFollow.enabled, + }; } /** The viewer's own select or follow, as it would have run. */ - function runStock(ev, to) { + function runStock(ev, to, atDown) { const name = ev.type === 'click' ? 'select' @@ -319,6 +382,7 @@ export function createCesiumInput( // Cesium bound them with no modifier: a modified click never ran them. const { shift, ctrl, alt } = ev.modifiers; if (shift || ctrl || alt || !behaviours[name].enabled) return; + if (atDown && !atDown[name]) return; try { stock[name]({ position: new Cesium.Cartesian2(ev.position.x, ev.position.y), @@ -352,7 +416,10 @@ export function createCesiumInput( deliver('dblclick', e.position, { button: 0, modifier }), ); bind(T.MOUSE_MOVE, (e, modifier) => - deliver('move', e.startPosition, { endPosition: e.endPosition, modifier }), + deliver('move', e.startPosition ?? e.endPosition, { + endPosition: e.endPosition, + modifier, + }), ); // Pinch is at the two fingers' midpoint where it started; its delta is // the scale since the last event. @@ -390,7 +457,7 @@ export function createCesiumInput( }; canvas?.addEventListener?.('wheel', onWheel, { passive: true }); - return { + const self = { port: router.port, feed(type, x, y, extra = {}) { deliver(type, { x, y }, extra, null); @@ -400,6 +467,7 @@ export function createCesiumInput( if (disposed) return; disposed = true; router.dispose(); + if (inputs.get(scene) === self) inputs.delete(scene); canvas?.removeEventListener?.('wheel', onWheel, { passive: true }); for (const type of DOM_EVENTS) dom?.removeEventListener?.(type, record, { capture: true }); @@ -407,10 +475,13 @@ export function createCesiumInput( uninstallCtrlWheel?.(); uninstallCtrlWheel = null; cameraInput.release(); + for (const name of Object.keys(borrowed)) giveBack(name); if (stockHandler && !stockHandler.isDestroyed?.()) for (const [name, type] of Object.entries(STOCK)) if (stock[name] && !stockHandler.getInputAction(type)) stockHandler.setInputAction(stock[name], type); }, }; + inputs.set(scene, self); + return self; } diff --git a/src/render/cesium/input.test.mjs b/src/render/cesium/input.test.mjs index 499d5917d..dffb468dc 100644 --- a/src/render/cesium/input.test.mjs +++ b/src/render/cesium/input.test.mjs @@ -48,7 +48,7 @@ function fakeDocument() { }; } -function setup({ stock = true, document = null } = {}) { +function setup({ stock = true, document = null, takeStock = true } = {}) { const stockHandler = fakeHandler(); const selected = []; const followed = []; @@ -86,6 +86,7 @@ function setup({ stock = true, document = null } = {}) { createHandler: () => handler, now: () => clock, document, + takeStock, ctrlWheelZoom: () => { wheelZoom.push('on'); return () => wheelZoom.push('off'); @@ -374,3 +375,53 @@ test('a double-tap is a touch; a click on Cesium’s timer ends no press', () => s.handler.fire(T.RIGHT_CLICK, { position: s.at(5, 6) }); assert.deepEqual(heard, ['touch', { travelPx: 0, durationMs: 0 }]); }); + +test('a press that began suppressed never selects, even if its tool lets go on the up', () => { + const s = setup(); + let release = s.port.behaviours.select.suppress(); + s.port.on('up', () => { + release?.(); + release = null; + }); + s.press(5, 6); + assert.equal(s.selected.length, 0, 'the click of the suppressed press'); + s.press(5, 6); + assert.equal(s.selected.length, 1, 'the next press selects'); +}); + +test('a hold a down subscriber takes keeps its press from selecting', () => { + const s = setup(); + let release = null; + s.port.on('down', () => { + release = s.port.behaviours.select.suppress(); + }); + s.port.on('up', () => release?.()); + s.press(5, 6); + assert.equal(s.selected.length, 0); +}); + +test('not taking the stock, they stay on the viewer’s handler but while held', () => { + const s = setup({ takeStock: false }); + const original = s.stockHandler.getInputAction(T.LEFT_CLICK); + assert.equal(typeof original, 'function', 'left where the viewer put them'); + s.press(5, 6); + assert.equal(s.selected.length, 0, 'the port never runs them itself'); + const release = s.port.behaviours.select.suppress(); + assert.equal(s.stockHandler.getInputAction(T.LEFT_CLICK), undefined); + assert.equal( + typeof s.stockHandler.getInputAction(T.LEFT_DOUBLE_CLICK), + 'function', + 'follow is its own', + ); + release(); + assert.equal(s.stockHandler.getInputAction(T.LEFT_CLICK), original); + s.port.behaviours.dblclickFollow.suppress(); + const follow = s.stockHandler.getInputAction(T.LEFT_DOUBLE_CLICK); + assert.equal(follow, undefined); + s.input.dispose(); + assert.equal( + typeof s.stockHandler.getInputAction(T.LEFT_DOUBLE_CLICK), + 'function', + 'dispose gives a held one back', + ); +}); diff --git a/src/ui/imageryBoxTool.test.mjs b/src/ui/imageryBoxTool.test.mjs index f6a2b6cb9..a5cb5378a 100644 --- a/src/ui/imageryBoxTool.test.mjs +++ b/src/ui/imageryBoxTool.test.mjs @@ -29,8 +29,14 @@ function fixture(options = {}) { ...options, }); const camera = fakes.viewer.scene.screenSpaceCameraController; + // The stock action as it runs now: the input port holds them, so it is + // the original while not suppressed, and none while it is. const stockAction = (type) => - fakes.viewer.screenSpaceEventHandler.getInputAction(fakes.types[type]); + fakes.stockEnabled(type === 'LEFT_CLICK' ? 'select' : 'dblclickFollow') + ? type === 'LEFT_CLICK' + ? fakes.originalClick + : fakes.originalDouble + : undefined; const preview = () => fakes.viewer.dataSources.sources[0]?.entities.values.every((e) => e.show); return {