diff --git a/src/data/selectionDispatcher.js b/src/data/selectionDispatcher.js index 278c07e93..b15ae1b81 100644 --- a/src/data/selectionDispatcher.js +++ b/src/data/selectionDispatcher.js @@ -9,8 +9,8 @@ // onEmptyClick(ctx) a clean click (short, no drag) on the map or nothing; // onEscape() Escape. // The rules are the old handlers': a pick another layer owns is not empty -// space; `passThrough` picks (trails) never take the click, the next pick -// does; a `drillThrough` owner (local ADS-B) takes its object from under +// space (a pick nobody owns is); `passThrough` picks (trails) never take the +// click, the next pick does; a `drillThrough` owner (local ADS-B) takes its object from under // another layer's; a drag selects nothing; a pointer owned by a tool // (inputOwnership) selects nothing. Layers not moved yet keep their own // handlers: the dispatcher never acts on an id it does not own. @@ -125,6 +125,10 @@ export function createSelectionDispatcher( return; } } + // A pick no layer owns (an unclaimed entity) is empty space, as it was + // for the old handlers: only another layer's object is not. + if (first.owner === null && isTrackingClickGesture(gesture)) + for (const owner of [...owners.values()]) owner.onEmptyClick?.(ctx); }; const onKey = (event) => { @@ -186,6 +190,23 @@ export function createSelectionDispatcher( }; } +const dispatchers = new WeakMap(); + +/** + * The application's one dispatcher for `engine`, created on first use (the + * layers that moved onto the engine share it). + * @param {import('../globe/contract.js').GlobeEngine} engine + */ +export function selectionDispatcherFor(engine) { + if (!engine) return null; + let dispatcher = dispatchers.get(engine); + if (!dispatcher) { + dispatcher = createSelectionDispatcher(engine); + dispatchers.set(engine, dispatcher); + } + return dispatcher; +} + function safely(run) { try { return run(); diff --git a/src/data/selectionDispatcher.test.mjs b/src/data/selectionDispatcher.test.mjs index 98c9440f5..fdf5bce14 100644 --- a/src/data/selectionDispatcher.test.mjs +++ b/src/data/selectionDispatcher.test.mjs @@ -1,7 +1,10 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { createFakeEngine } from '../globe/testing/fakeEngine.js'; -import { createSelectionDispatcher } from './selectionDispatcher.js'; +import { + createSelectionDispatcher, + selectionDispatcherFor, +} from './selectionDispatcher.js'; import { isOwnedByOtherLayer, registerPickOwner, @@ -95,6 +98,22 @@ test("another layer's pick is not empty space, owned through the shared registry } }); +test('a pick no layer owns is empty space, as for the old handlers', () => { + const { dispatcher, click } = setup({ 10: [{ id: 'unclaimed-entity' }] }); + const flights = recorder(); + dispatcher.register('flights', { + owns: (id) => id === 'x', + ...flights.handlers, + }); + click(10); + click(10, { ms: 900 }); + assert.deepEqual( + flights.calls, + [['empty', 0]], + 'a long press does not deselect', + ); +}); + test('trails do not take clicks: the object under a trail is selected', () => { const { dispatcher, click } = setup({ 10: [{ id: 'trail-ae01ce', owner: 'flights-trail' }, { id: 'ae01ce' }], @@ -173,3 +192,16 @@ test('unregister and destroy remove the subscriptions', () => { assert.equal(isOwnedByOtherLayer('other', 'a'), false); assert.ok(engine.fake.callsOf('input.on').length >= 1); }); + +test('the application has one dispatcher per engine', () => { + const engine = createFakeEngine(); + const other = createFakeEngine(); + assert.equal(selectionDispatcherFor(engine), selectionDispatcherFor(engine)); + assert.notEqual( + selectionDispatcherFor(engine), + selectionDispatcherFor(other), + ); + assert.equal(selectionDispatcherFor(null), null); + selectionDispatcherFor(engine).destroy(); + selectionDispatcherFor(other).destroy(); +});