feat(data): the selection dispatcher treats an unowned pick as empty space

Task 10a moves flights onto the dispatcher; its old handler deselected on a
pick nobody owns (an unclaimed entity), and only another layer's pick was not
empty space. The dispatcher now does the same (after drill-through owners had
their chance), and selectionDispatcherFor(engine) gives the application one
dispatcher per engine. Tests for both.
This commit is contained in:
German Khokhlov committed 2026-10-17 00:20:18 +04:00
1 parent 9f1a0611fa
commit 8e702f0c3b
2 files changed
+56 -3

No files matched your search

+23 -2
View File
@@ -9,8 +9,8 @@
// onEmptyClick(ctx) a clean click (short, no drag) on the map or nothing; // onEmptyClick(ctx) a clean click (short, no drag) on the map or nothing;
// onEscape() Escape. // onEscape() Escape.
// The rules are the old handlers': a pick another layer owns is not empty // 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 // space (a pick nobody owns is); `passThrough` picks (trails) never take the
// does; a `drillThrough` owner (local ADS-B) takes its object from under // 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 // another layer's; a drag selects nothing; a pointer owned by a tool
// (inputOwnership) selects nothing. Layers not moved yet keep their own // (inputOwnership) selects nothing. Layers not moved yet keep their own
// handlers: the dispatcher never acts on an id it does not own. // handlers: the dispatcher never acts on an id it does not own.
@@ -125,6 +125,10 @@ export function createSelectionDispatcher(
return; 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) => { 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) { function safely(run) {
try { try {
return run(); return run();
+33 -1
View File
@@ -1,7 +1,10 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { createFakeEngine } from '../globe/testing/fakeEngine.js'; import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { createSelectionDispatcher } from './selectionDispatcher.js'; import {
createSelectionDispatcher,
selectionDispatcherFor,
} from './selectionDispatcher.js';
import { import {
isOwnedByOtherLayer, isOwnedByOtherLayer,
registerPickOwner, 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', () => { test('trails do not take clicks: the object under a trail is selected', () => {
const { dispatcher, click } = setup({ const { dispatcher, click } = setup({
10: [{ id: 'trail-ae01ce', owner: 'flights-trail' }, { id: 'ae01ce' }], 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.equal(isOwnedByOtherLayer('other', 'a'), false);
assert.ok(engine.fake.callsOf('input.on').length >= 1); 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();
});