mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Scope pick ownership and selection dispatch to each scene
Create pick registries and selection dispatchers for each scene attachment, publish them through scene.services, and release their predicates and input subscriptions before parking the renderer. Resolve layer ownership with the attached engine so identical layer IDs in simultaneous scenes cannot overwrite or remove one another. Re-register retained ALPR and cyclone picks on attachment, and resolve voice pointer picks against the engine that supplied the pick. Preserve Cesium's selection, sibling ownership, inert trails, touch expansion, drag and long-press rules on both engines. Extract the unchanged gesture predicates so portable picking does not acquire a native input dependency. Exercise scene isolation, teardown and reattachment on fake and headless Cesium engines, retaining the existing layer assertions. No coupling count falls or grows. Cesium and Navara renderer lines remain 0 and 189; external Cesium imports 3; viewer lines 910; Navara kind checks 2; engine sniff calls 0; extension reads 3; ground passes 1; SDK loads 3; screen-handler files 4; engine-name lines 33.
This commit is contained in:
1 parent
52c6f68bd5
commit
4252ed0798
53 files changed
+712
-277
No files matched your search
@@ -270,8 +270,15 @@ one application, and only the globe-bound parts move to the next engine.
|
|||||||
unknown kind and no native extensions, draws fixtures, hides or releases them,
|
unknown kind and no native extensions, draws fixtures, hides or releases them,
|
||||||
and checks primitives, subscriptions and service records before engine
|
and checks primitives, subscriptions and service records before engine
|
||||||
destruction. The three self-drawing entries are named skips. This is a
|
destruction. The three self-drawing entries are named skips. This is a
|
||||||
display-level rehearsal; application kind registration, scene-service
|
display-level rehearsal; application kind registration, the remaining scene
|
||||||
singletons, queue consolidation and native debug access remain work.
|
services, queue consolidation and native debug access remain work.
|
||||||
|
- **Scene pick ownership.** Each attachment owns `scene.services.picking` and
|
||||||
|
`scene.services.selection` (`src/data/sceneServices.js`). Layers and displays
|
||||||
|
resolve these services with their attached engine; simultaneous engines do
|
||||||
|
not share owner predicates or click subscriptions. Detach releases both
|
||||||
|
services before the renderer is parked. Reattachment creates fresh services,
|
||||||
|
while retained layers register their current picks again. The overlay host,
|
||||||
|
render governor and tracked readout still have module-owned state.
|
||||||
- **The session owns the state.** Catalog, layer models, sources, selection,
|
- **The session owns the state.** Catalog, layer models, sources, selection,
|
||||||
camera mode and URL state outlive a switch. A layer that is a session model
|
camera mode and URL state outlive a switch. A layer that is a session model
|
||||||
gets `attachRenderer(viewer, { engine })` / `detachRenderer()` instead of being rebuilt,
|
gets `attachRenderer(viewer, { engine })` / `detachRenderer()` instead of being rebuilt,
|
||||||
|
|||||||
@@ -538,6 +538,15 @@ Cockpit, imagery-box drawing, CCTV calibration and infrastructure flights use
|
|||||||
these methods on every engine. Pointer claims remain separate from camera
|
these methods on every engine. Pointer claims remain separate from camera
|
||||||
input suspension.
|
input suspension.
|
||||||
|
|
||||||
|
Pick ownership belongs to the scene attachment, not to an engine implementation.
|
||||||
|
`scene.services.picking` owns its predicates and `scene.services.selection` owns
|
||||||
|
its shared click dispatcher. Layer entry points resolve these services using
|
||||||
|
their attached engine (`sceneServicesFor(engine)`); the pick-registry helper
|
||||||
|
functions take that engine as their final argument. Standalone entries acquire
|
||||||
|
the same services on first use. The application destroys them on detach and
|
||||||
|
creates fresh services when reattaching a parked engine. Identical layer IDs in
|
||||||
|
simultaneous scenes cannot replace or unregister one another's picks.
|
||||||
|
|
||||||
## Render effects
|
## Render effects
|
||||||
|
|
||||||
The optional `visualStyles`, `bloom` and `contactSonar` capabilities are
|
The optional `visualStyles`, `bloom` and `contactSonar` capabilities are
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
[
|
[
|
||||||
|
"src/data/sceneServices.js",
|
||||||
|
"src/data/sceneServices.test.mjs",
|
||||||
|
"src/data/trackingGesture.js",
|
||||||
".prettierrc.json",
|
".prettierrc.json",
|
||||||
"package.json",
|
"package.json",
|
||||||
"scripts/format-scope.json",
|
"scripts/format-scope.json",
|
||||||
|
|||||||
@@ -1405,6 +1405,7 @@
|
|||||||
"src/geoDistance.js",
|
"src/geoDistance.js",
|
||||||
"src/data/trackedModelRegime.js",
|
"src/data/trackedModelRegime.js",
|
||||||
"src/data/trackingClickGesture.js",
|
"src/data/trackingClickGesture.js",
|
||||||
|
"src/data/trackingGesture.js",
|
||||||
"src/layers/flights/controller.js",
|
"src/layers/flights/controller.js",
|
||||||
"src/layers/flights/enrichment.js",
|
"src/layers/flights/enrichment.js",
|
||||||
"src/layers/flights/evidence.js",
|
"src/layers/flights/evidence.js",
|
||||||
@@ -1476,6 +1477,7 @@
|
|||||||
"src/data/renderAltitude.js",
|
"src/data/renderAltitude.js",
|
||||||
"src/data/trackedModelRegime.js",
|
"src/data/trackedModelRegime.js",
|
||||||
"src/data/trackingClickGesture.js",
|
"src/data/trackingClickGesture.js",
|
||||||
|
"src/data/trackingGesture.js",
|
||||||
"src/layers/military/controller.js",
|
"src/layers/military/controller.js",
|
||||||
"src/layers/military/index.js",
|
"src/layers/military/index.js",
|
||||||
"src/layers/military/ingestion.js",
|
"src/layers/military/ingestion.js",
|
||||||
@@ -1543,6 +1545,7 @@
|
|||||||
"src/geoDistance.js",
|
"src/geoDistance.js",
|
||||||
"src/data/trackedModelRegime.js",
|
"src/data/trackedModelRegime.js",
|
||||||
"src/data/trackingClickGesture.js",
|
"src/data/trackingClickGesture.js",
|
||||||
|
"src/data/trackingGesture.js",
|
||||||
"src/layers/aircraft/classification.js",
|
"src/layers/aircraft/classification.js",
|
||||||
"src/layers/aircraft/index.js",
|
"src/layers/aircraft/index.js",
|
||||||
"src/data/inputOwnership.js",
|
"src/data/inputOwnership.js",
|
||||||
@@ -2584,6 +2587,8 @@
|
|||||||
"src/data/local_data/us_census_counties/counties.json",
|
"src/data/local_data/us_census_counties/counties.json",
|
||||||
"src/data/neighborhoodPolygons.js",
|
"src/data/neighborhoodPolygons.js",
|
||||||
"src/data/pickRegistry.js",
|
"src/data/pickRegistry.js",
|
||||||
|
"src/data/sceneServices.js",
|
||||||
|
"src/data/trackingGesture.js",
|
||||||
"src/data/radio.js",
|
"src/data/radio.js",
|
||||||
"src/data/radioCountry.js",
|
"src/data/radioCountry.js",
|
||||||
"src/data/regionalBrief.js",
|
"src/data/regionalBrief.js",
|
||||||
@@ -3741,6 +3746,8 @@
|
|||||||
"src/data/local_data/us_census_counties/counties.json",
|
"src/data/local_data/us_census_counties/counties.json",
|
||||||
"src/data/neighborhoodPolygons.js",
|
"src/data/neighborhoodPolygons.js",
|
||||||
"src/data/pickRegistry.js",
|
"src/data/pickRegistry.js",
|
||||||
|
"src/data/sceneServices.js",
|
||||||
|
"src/data/trackingGesture.js",
|
||||||
"src/data/radioCountry.js",
|
"src/data/radioCountry.js",
|
||||||
"src/data/renderAltitude.js",
|
"src/data/renderAltitude.js",
|
||||||
"src/data/bundledJson.js",
|
"src/data/bundledJson.js",
|
||||||
@@ -5030,6 +5037,8 @@
|
|||||||
"src/data/labelArbiter.js",
|
"src/data/labelArbiter.js",
|
||||||
"src/data/meshFloorSampler.js",
|
"src/data/meshFloorSampler.js",
|
||||||
"src/data/pickRegistry.js",
|
"src/data/pickRegistry.js",
|
||||||
|
"src/data/sceneServices.js",
|
||||||
|
"src/data/trackingGesture.js",
|
||||||
"src/data/scenePick.js",
|
"src/data/scenePick.js",
|
||||||
"src/data/spriteOrder.js",
|
"src/data/spriteOrder.js",
|
||||||
"src/data/terrainHeights.js",
|
"src/data/terrainHeights.js",
|
||||||
@@ -5131,7 +5140,8 @@
|
|||||||
"src/styles/noir.js",
|
"src/styles/noir.js",
|
||||||
"src/globe/cesium/meshes.js",
|
"src/globe/cesium/meshes.js",
|
||||||
"src/globe/meshes.js",
|
"src/globe/meshes.js",
|
||||||
"src/globe/exclusivePointer.js"
|
"src/globe/exclusivePointer.js",
|
||||||
|
"src/data/selectionDispatcher.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "egm96-universal", "pbf"]
|
"external": ["cesium", "egm96-universal", "pbf"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ test('the scene constructs the engine with the viewer and returns it beside the
|
|||||||
assert.match(scene, /tilesetRef\.current = tileset;/);
|
assert.match(scene, /tilesetRef\.current = tileset;/);
|
||||||
assert.match(
|
assert.match(
|
||||||
scene.replace(/\s+/g, ' '),
|
scene.replace(/\s+/g, ' '),
|
||||||
/return \{ viewer, engine, tileset, mapStackController, operations,? \};/,
|
/return \{ viewer, engine, tileset, mapStackController, operations, services,? \};/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { createApplicationScene } from './scene.js';
|
import { createApplicationScene } from './scene.js';
|
||||||
|
import { sceneServicesFor } from '../data/sceneServices.js';
|
||||||
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||||
import {
|
import {
|
||||||
installEngineRenderGovernor,
|
installEngineRenderGovernor,
|
||||||
@@ -260,3 +261,19 @@ test('parking or tearing down the Navara scene releases a running camera verb',
|
|||||||
if (end === 'park') cache.clear();
|
if (end === 'park') cache.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the application scene owns and releases its pick services', async () => {
|
||||||
|
const run = await startScene({ googleApiKey: '' });
|
||||||
|
const { engine, services } = run.scene;
|
||||||
|
try {
|
||||||
|
assert.equal(sceneServicesFor(engine), services);
|
||||||
|
services.selection.register('flights', { owns: (id) => id === 'contact' });
|
||||||
|
assert.equal(services.picking.findPickOwner('contact'), 'flights');
|
||||||
|
} finally {
|
||||||
|
await run.teardown();
|
||||||
|
}
|
||||||
|
assert.equal(services.picking.findPickOwner('contact'), null);
|
||||||
|
services.picking.registerPickOwner('late', () => true);
|
||||||
|
assert.equal(services.picking.findPickOwner('contact'), null);
|
||||||
|
assert.equal(engine.isDestroyed(), true);
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createSceneServices } from '../data/sceneServices.js';
|
||||||
import { releaseCameraVerbs } from '../cameraVerbs.js';
|
import { releaseCameraVerbs } from '../cameraVerbs.js';
|
||||||
import { createApplicationOperations } from './operations.js';
|
import { createApplicationOperations } from './operations.js';
|
||||||
import {
|
import {
|
||||||
@@ -79,6 +80,8 @@ export async function createApplicationScene({
|
|||||||
const { viewer, engine, tileset, mapStackController } = rendererCache
|
const { viewer, engine, tileset, mapStackController } = rendererCache
|
||||||
? await rendererCache.acquire(options.engineKind, build, defer)
|
? await rendererCache.acquire(options.engineKind, build, defer)
|
||||||
: await build({ defer });
|
: await build({ defer });
|
||||||
|
const services = createSceneServices(engine);
|
||||||
|
defer(services.destroy);
|
||||||
if (layerSession) {
|
if (layerSession) {
|
||||||
// DEM cells belong to the session. Mesh samples came from the outgoing
|
// DEM cells belong to the session. Mesh samples came from the outgoing
|
||||||
// scene, including when the incoming canvas was warm in the cache.
|
// scene, including when the incoming canvas was warm in the cache.
|
||||||
@@ -93,6 +96,7 @@ export async function createApplicationScene({
|
|||||||
tileset,
|
tileset,
|
||||||
mapStackController,
|
mapStackController,
|
||||||
operations,
|
operations,
|
||||||
|
services,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -600,7 +600,7 @@ test('vessel and fire layers announce valid clicks and never fly cameras', () =>
|
|||||||
ordered(
|
ordered(
|
||||||
vesselClick,
|
vesselClick,
|
||||||
[
|
[
|
||||||
"isOwnedByOtherLayer('ais-live-vessels', pickedId)",
|
/isOwnedByOtherLayer\(\s*'ais-live-vessels',\s*pickedId,\s*state\.renderer\?\.engine,?\s*\)/,
|
||||||
'_vesselOverlayHost.hitTest?.(',
|
'_vesselOverlayHost.hitTest?.(',
|
||||||
'selectAndFocusVessel(record)',
|
'selectAndFocusVessel(record)',
|
||||||
],
|
],
|
||||||
@@ -626,7 +626,7 @@ test('vessel and fire layers announce valid clicks and never fly cameras', () =>
|
|||||||
ordered(
|
ordered(
|
||||||
fireClick,
|
fireClick,
|
||||||
[
|
[
|
||||||
'isOwnedByOtherLayer(id, pickedId)',
|
/isOwnedByOtherLayer\(\s*id,\s*pickedId,\s*layerState\._renderer\?\.engine,?\s*\)/,
|
||||||
'overlayHost.hitTest?.(',
|
'overlayHost.hitTest?.(',
|
||||||
'selectAndFocusFire(carded)',
|
'selectAndFocusFire(carded)',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ import aisLiveVesselsLayer from './aisLiveVessels.js';
|
|||||||
import { registerEntityContext, selectEntityContext } from './contextStore.js';
|
import { registerEntityContext, selectEntityContext } from './contextStore.js';
|
||||||
import { WORLD_FOCUS_REQUEST_EVENT } from '../worldFocus.js';
|
import { WORLD_FOCUS_REQUEST_EVENT } from '../worldFocus.js';
|
||||||
import { ensureGeoidReady, geoidHeight } from './geoid.js';
|
import { ensureGeoidReady, geoidHeight } from './geoid.js';
|
||||||
import { registerPickOwner, unregisterPickOwner } from './pickRegistry.js';
|
import { pickRegistryFor } from './pickRegistry.js';
|
||||||
|
const wireEngine = {};
|
||||||
|
const { registerPickOwner, unregisterPickOwner } = pickRegistryFor(wireEngine);
|
||||||
import { applyVesselOverlayPolicy } from './vesselLabels.js';
|
import { applyVesselOverlayPolicy } from './vesselLabels.js';
|
||||||
import { layerFeedState } from './manager.js';
|
import { layerFeedState } from './manager.js';
|
||||||
import {
|
import {
|
||||||
@@ -959,7 +961,7 @@ function installWireHarness(picked, stateOverrides = {}) {
|
|||||||
trackedEntityChanged,
|
trackedEntityChanged,
|
||||||
};
|
};
|
||||||
const renderer = ({ billboardCollection }) =>
|
const renderer = ({ billboardCollection }) =>
|
||||||
vesselViewerRenderer({ viewer: followViewer, billboardCollection });
|
({ ...vesselViewerRenderer({ viewer: followViewer, billboardCollection }), engine: wireEngine });
|
||||||
_setVesselStateForTest({
|
_setVesselStateForTest({
|
||||||
records: [record],
|
records: [record],
|
||||||
selectedRecord: record,
|
selectedRecord: record,
|
||||||
|
|||||||
@@ -383,12 +383,12 @@ const SIBLING_PICKS = [
|
|||||||
for (const [layerId, ownedId, picked] of SIBLING_PICKS) {
|
for (const [layerId, ownedId, picked] of SIBLING_PICKS) {
|
||||||
test(`a ${layerId} pick shaped ${JSON.stringify(picked)} wins over a card hit`, () => {
|
test(`a ${layerId} pick shaped ${JSON.stringify(picked)} wins over a card hit`, () => {
|
||||||
const fire = makeFire();
|
const fire = makeFire();
|
||||||
registerPickOwner(layerId, (pickedId) => pickedId === ownedId);
|
|
||||||
const h = harness({
|
const h = harness({
|
||||||
fires: [fire],
|
fires: [fire],
|
||||||
picked,
|
picked,
|
||||||
cardHit: `fire:${fireDetectionKey(fire)}`,
|
cardHit: `fire:${fireDetectionKey(fire)}`,
|
||||||
});
|
});
|
||||||
|
registerPickOwner(layerId, (pickedId) => pickedId === ownedId, h.engine);
|
||||||
try {
|
try {
|
||||||
h.click();
|
h.click();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -398,7 +398,7 @@ for (const [layerId, ownedId, picked] of SIBLING_PICKS) {
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
h.cleanup();
|
h.cleanup();
|
||||||
unregisterPickOwner(layerId);
|
unregisterPickOwner(layerId, h.engine);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+143
-89
@@ -11,11 +11,9 @@
|
|||||||
* that belong to someone else and leave them alone.
|
* that belong to someone else and leave them alone.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { sceneServicesFor } from './sceneServices.js';
|
||||||
import { isSurfacePick } from './scenePick.js';
|
import { isSurfacePick } from './scenePick.js';
|
||||||
|
|
||||||
/** @type {Map<string, (pickedId: string) => boolean>} layerId -> ownership predicate */
|
|
||||||
const _owners = new Map();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a scene.pick() result to a String pick id for the ownership scan.
|
* Resolves a scene.pick() result to a String pick id for the ownership scan.
|
||||||
*
|
*
|
||||||
@@ -46,99 +44,155 @@ export function resolvePickId(picked) {
|
|||||||
return typeof id === 'string' || typeof id === 'number' ? String(id) : null;
|
return typeof id === 'string' || typeof id === 'number' ? String(id) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Own the pick predicates for one scene. */
|
||||||
* Registers a pick-ownership predicate for a layer.
|
export function createPickRegistry() {
|
||||||
* @param {string} layerId - Owning layer id (e.g. 'flights').
|
const _owners = new Map();
|
||||||
* @param {(pickedId: string) => boolean} predicate - Returns true when the
|
let destroyed = false;
|
||||||
* picked primitive/entity id belongs to this layer.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function registerPickOwner(layerId, predicate) {
|
|
||||||
if (!layerId || typeof predicate !== 'function') return;
|
|
||||||
_owners.set(layerId, predicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a layer's ownership predicate (call on disable/destroy).
|
* Registers a pick-ownership predicate for a layer.
|
||||||
* @param {string} layerId - Owning layer id.
|
* @param {string} layerId - Owning layer id (e.g. 'flights').
|
||||||
* @returns {void}
|
* @param {(pickedId: string) => boolean} predicate - Returns true when the
|
||||||
*/
|
* picked primitive/entity id belongs to this layer.
|
||||||
export function unregisterPickOwner(layerId) {
|
* @returns {void}
|
||||||
_owners.delete(layerId);
|
*/
|
||||||
}
|
function registerPickOwner(layerId, predicate) {
|
||||||
|
if (destroyed || !layerId || typeof predicate !== 'function') return;
|
||||||
/**
|
_owners.set(layerId, predicate);
|
||||||
* The registered layer that owns a picked id, or null.
|
|
||||||
* @param {string} pickedId - Canonical pick id from {@link resolvePickId}.
|
|
||||||
* @returns {string|null} Owning layer id.
|
|
||||||
*/
|
|
||||||
export function findPickOwner(pickedId) {
|
|
||||||
if (!pickedId) return null;
|
|
||||||
for (const [ownerId, predicate] of _owners) {
|
|
||||||
try {
|
|
||||||
if (predicate(pickedId)) return ownerId;
|
|
||||||
} catch {
|
|
||||||
// a broken predicate must never break picking
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when some OTHER registered layer owns the picked id.
|
* Removes a layer's ownership predicate (call on disable/destroy).
|
||||||
* @param {string} layerId - The asking layer's id (excluded from the scan).
|
* @param {string} layerId - Owning layer id.
|
||||||
* @param {string} pickedId - Picked primitive/entity id.
|
* @returns {void}
|
||||||
* @returns {boolean}
|
*/
|
||||||
*/
|
function unregisterPickOwner(layerId) {
|
||||||
export function isOwnedByOtherLayer(layerId, pickedId) {
|
_owners.delete(layerId);
|
||||||
if (!pickedId) return false;
|
|
||||||
for (const [ownerId, predicate] of _owners) {
|
|
||||||
if (ownerId === layerId) continue;
|
|
||||||
try {
|
|
||||||
if (predicate(pickedId)) return true;
|
|
||||||
} catch {
|
|
||||||
// a broken predicate must never break click handling
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
/**
|
||||||
|
* The registered layer that owns a picked id, or null.
|
||||||
|
* @param {string} pickedId - Canonical pick id from {@link resolvePickId}.
|
||||||
|
* @returns {string|null} Owning layer id.
|
||||||
|
*/
|
||||||
|
function findPickOwner(pickedId) {
|
||||||
|
if (!pickedId) return null;
|
||||||
|
for (const [ownerId, predicate] of _owners) {
|
||||||
|
try {
|
||||||
|
if (predicate(pickedId)) return ownerId;
|
||||||
|
} catch {
|
||||||
|
// a broken predicate must never break picking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when some OTHER registered layer owns the picked id.
|
||||||
|
* @param {string} layerId - The asking layer's id (excluded from the scan).
|
||||||
|
* @param {string} pickedId - Picked primitive/entity id.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isOwnedByOtherLayer(layerId, pickedId) {
|
||||||
|
if (!pickedId) return false;
|
||||||
|
for (const [ownerId, predicate] of _owners) {
|
||||||
|
if (ownerId === layerId) continue;
|
||||||
|
try {
|
||||||
|
if (predicate(pickedId)) return true;
|
||||||
|
} catch {
|
||||||
|
// a broken predicate must never break click handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a moving-contact click without letting map tiles hide a nearby touch
|
||||||
|
* target. Exact picks always win; only a coarse primary pointer may expand a
|
||||||
|
* surface miss. Every consumer uses the same ordered, bounded scene hits, so sibling layers
|
||||||
|
* do not each choose their own contact under one tap.
|
||||||
|
* Unowned non-surface objects remain barriers, not permission to pick through.
|
||||||
|
*
|
||||||
|
* @param {object} scene Cesium scene with pick and optional drillPick.
|
||||||
|
* @param {{x: number, y: number}} position Screen position in CSS pixels.
|
||||||
|
* @returns {object|null|undefined} Original pick or the first registered touch hit.
|
||||||
|
*/
|
||||||
|
function pickContact(scene, position) {
|
||||||
|
return nearbyContact(
|
||||||
|
scene.pick(position),
|
||||||
|
position,
|
||||||
|
scene.drillPick?.bind(scene),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearbyContact(picked, position, drillPick) {
|
||||||
|
if (
|
||||||
|
!globalThis.matchMedia?.('(pointer: coarse)').matches ||
|
||||||
|
findPickOwner(resolvePickId(picked)) ||
|
||||||
|
!isSurfacePick(picked) ||
|
||||||
|
typeof drillPick !== 'function'
|
||||||
|
)
|
||||||
|
return picked;
|
||||||
|
let hits;
|
||||||
|
try {
|
||||||
|
hits = drillPick(position, 8, 14, 14) || [];
|
||||||
|
} catch {
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
for (const hit of hits.slice(0, 8)) {
|
||||||
|
if (findPickOwner(resolvePickId(hit))) return hit;
|
||||||
|
if (!isSurfacePick(hit)) break;
|
||||||
|
}
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPickOwner('trails', (id) => String(id).startsWith('gev-trail:'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
resolvePickId,
|
||||||
|
registerPickOwner,
|
||||||
|
unregisterPickOwner,
|
||||||
|
findPickOwner,
|
||||||
|
isOwnedByOtherLayer,
|
||||||
|
pickContact,
|
||||||
|
nearbyContact,
|
||||||
|
destroy() {
|
||||||
|
destroyed = true;
|
||||||
|
_owners.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Pick ownership of the scene attached to this engine. */
|
||||||
* Resolve a moving-contact click without letting map tiles hide a nearby touch
|
export function pickRegistryFor(engine) {
|
||||||
* target. Exact picks always win; only a coarse primary pointer may expand a
|
return sceneServicesFor(engine)?.picking ?? null;
|
||||||
* surface miss. Every consumer uses the same ordered, bounded scene hits, so sibling layers
|
}
|
||||||
* do not each choose their own contact under one tap.
|
|
||||||
* Unowned non-surface objects remain barriers, not permission to pick through.
|
export function registerPickOwner(layerId, predicate, engine) {
|
||||||
*
|
return pickRegistryFor(engine)?.registerPickOwner(layerId, predicate);
|
||||||
* @param {object} scene Cesium scene with pick and optional drillPick.
|
}
|
||||||
* @param {{x: number, y: number}} position Screen position in CSS pixels.
|
|
||||||
* @returns {object|null|undefined} Original pick or the first registered touch hit.
|
export function unregisterPickOwner(layerId, engine) {
|
||||||
*/
|
return pickRegistryFor(engine)?.unregisterPickOwner(layerId);
|
||||||
export function pickContact(scene, position) {
|
}
|
||||||
return nearbyContact(
|
|
||||||
scene.pick(position),
|
export function findPickOwner(pickedId, engine) {
|
||||||
position,
|
return pickRegistryFor(engine)?.findPickOwner(pickedId) ?? null;
|
||||||
scene.drillPick?.bind(scene),
|
}
|
||||||
|
|
||||||
|
export function isOwnedByOtherLayer(layerId, pickedId, engine) {
|
||||||
|
return (
|
||||||
|
pickRegistryFor(engine)?.isOwnedByOtherLayer(layerId, pickedId) ?? false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nearbyContact(picked, position, drillPick) {
|
export function pickContact(scene, position, engine) {
|
||||||
if (
|
return pickRegistryFor(engine)?.pickContact(scene, position) ?? null;
|
||||||
!globalThis.matchMedia?.('(pointer: coarse)').matches ||
|
}
|
||||||
findPickOwner(resolvePickId(picked)) ||
|
|
||||||
!isSurfacePick(picked) ||
|
export function nearbyContact(picked, position, drillPick, engine) {
|
||||||
typeof drillPick !== 'function'
|
return (
|
||||||
)
|
pickRegistryFor(engine)?.nearbyContact(picked, position, drillPick) ??
|
||||||
return picked;
|
picked
|
||||||
let hits;
|
);
|
||||||
try {
|
|
||||||
hits = drillPick(position, 8, 14, 14) || [];
|
|
||||||
} catch {
|
|
||||||
return picked;
|
|
||||||
}
|
|
||||||
for (const hit of hits.slice(0, 8)) {
|
|
||||||
if (findPickOwner(resolvePickId(hit))) return hit;
|
|
||||||
if (!isSurfacePick(hit)) break;
|
|
||||||
}
|
|
||||||
return picked;
|
|
||||||
}
|
}
|
||||||
@@ -11,13 +11,8 @@
|
|||||||
|
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
import { createPickRegistry, resolvePickId } from './pickRegistry.js';
|
||||||
registerPickOwner,
|
const { registerPickOwner, unregisterPickOwner, isOwnedByOtherLayer, pickContact } = createPickRegistry();
|
||||||
unregisterPickOwner,
|
|
||||||
isOwnedByOtherLayer,
|
|
||||||
resolvePickId,
|
|
||||||
pickContact,
|
|
||||||
} from './pickRegistry.js';
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// resolvePickId — canonical String coercion for every layer's pick shape
|
// resolvePickId — canonical String coercion for every layer's pick shape
|
||||||
|
|||||||
@@ -97,16 +97,20 @@ function descriptorLabel(found, fallback) {
|
|||||||
* @param {object|null} picked Result of `scene.pick()`.
|
* @param {object|null} picked Result of `scene.pick()`.
|
||||||
* @param {object} [options]
|
* @param {object} [options]
|
||||||
* @param {object} [options.dataManager] Layer lifecycle (enabled state, modules).
|
* @param {object} [options.dataManager] Layer lifecycle (enabled state, modules).
|
||||||
|
* @param {object} [options.engine] Engine of the scene that supplied the pick.
|
||||||
* @returns {{layerId: string, id: string, label: string, kind: string, lat: number|null, lon: number|null, altitudeM?: number|null}|null}
|
* @returns {{layerId: string, id: string, label: string, kind: string, lat: number|null, lon: number|null, altitudeM?: number|null}|null}
|
||||||
*/
|
*/
|
||||||
export function resolvePickedContext(picked, { dataManager = null } = {}) {
|
export function resolvePickedContext(
|
||||||
|
picked,
|
||||||
|
{ dataManager = null, engine = dataManager?.engine } = {},
|
||||||
|
) {
|
||||||
if (!picked) return null;
|
if (!picked) return null;
|
||||||
const record = contextCarrierRecord(picked, dataManager);
|
const record = contextCarrierRecord(picked, dataManager);
|
||||||
if (record) return fromRecord(record);
|
if (record) return fromRecord(record);
|
||||||
|
|
||||||
const pickId = resolvePickId(picked);
|
const pickId = resolvePickId(picked);
|
||||||
if (!pickId) return null;
|
if (!pickId) return null;
|
||||||
const layerId = findPickOwner(pickId);
|
const layerId = findPickOwner(pickId, engine);
|
||||||
if (!layerId || IGNORED_OWNERS.has(layerId)) return null;
|
if (!layerId || IGNORED_OWNERS.has(layerId)) return null;
|
||||||
if (dataManager?.isEnabled && !dataManager.isEnabled(layerId)) return null;
|
if (dataManager?.isEnabled && !dataManager.isEnabled(layerId)) return null;
|
||||||
const layer = dataManager?.layers?.get?.(layerId);
|
const layer = dataManager?.layers?.get?.(layerId);
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ globalThis.CustomEvent ||= class CustomEvent {
|
|||||||
globalThis.dispatchEvent ||= () => true;
|
globalThis.dispatchEvent ||= () => true;
|
||||||
|
|
||||||
const { registerEntityContext } = await import('./contextStore.js');
|
const { registerEntityContext } = await import('./contextStore.js');
|
||||||
const { registerPickOwner, unregisterPickOwner, findPickOwner } = await import('./pickRegistry.js');
|
const engine = {};
|
||||||
|
const { pickRegistryFor } = await import('./pickRegistry.js');
|
||||||
|
const { registerPickOwner, unregisterPickOwner, findPickOwner } = pickRegistryFor(engine);
|
||||||
const { resolvePickedContext, pickedKindForLayer } = await import('./pickedContext.js');
|
const { resolvePickedContext, pickedKindForLayer } = await import('./pickedContext.js');
|
||||||
|
|
||||||
function manager({ enabled = new Set(['flights', 'local-datacenters']), modules = {} } = {}) {
|
function manager({ enabled = new Set(['flights', 'local-datacenters']), modules = {} } = {}) {
|
||||||
return {
|
return {
|
||||||
|
engine,
|
||||||
isEnabled: (id) => enabled.has(id),
|
isEnabled: (id) => enabled.has(id),
|
||||||
layers: new Map(Object.entries(modules).map(([id, module]) => [id, { name: id, module }])),
|
layers: new Map(Object.entries(modules).map(([id, module]) => [id, { name: id, module }])),
|
||||||
};
|
};
|
||||||
@@ -73,3 +76,23 @@ test('disabled layers, trails and empty picks never resolve', () => {
|
|||||||
assert.equal(resolvePickedContext({ id: 'unknown' }, { dataManager: manager() }), null);
|
assert.equal(resolvePickedContext({ id: 'unknown' }, { dataManager: manager() }), null);
|
||||||
assert.equal(pickedKindForLayer('ais-live-vessels'), 'vessel');
|
assert.equal(pickedKindForLayer('ais-live-vessels'), 'vessel');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an explicit pointer engine resolves only that scene, without a manager', () => {
|
||||||
|
const first = {};
|
||||||
|
const second = {};
|
||||||
|
const a = pickRegistryFor(first);
|
||||||
|
const b = pickRegistryFor(second);
|
||||||
|
a.registerPickOwner('flights', (id) => id === 'shared');
|
||||||
|
b.registerPickOwner('satellites', (id) => id === 'shared');
|
||||||
|
try {
|
||||||
|
const picked = { id: 'shared' };
|
||||||
|
assert.equal(resolvePickedContext(picked, { engine: first }).layerId, 'flights');
|
||||||
|
assert.equal(resolvePickedContext(picked, { engine: second }).layerId, 'satellites');
|
||||||
|
a.destroy();
|
||||||
|
assert.equal(resolvePickedContext(picked, { engine: first }), null);
|
||||||
|
assert.equal(resolvePickedContext(picked, { engine: second }).layerId, 'satellites');
|
||||||
|
} finally {
|
||||||
|
a.destroy();
|
||||||
|
b.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createPickRegistry } from './pickRegistry.js';
|
||||||
|
import { createSelectionDispatcher } from './selectionDispatcher.js';
|
||||||
|
|
||||||
|
// Portable layers may attach directly to an engine, outside the application.
|
||||||
|
// Their lookup reaches the same services the app publishes on its scene.
|
||||||
|
const scenes = new WeakMap();
|
||||||
|
|
||||||
|
/** Create the services for one attachment, including reuse of a parked engine. */
|
||||||
|
export function createSceneServices(engine, options = {}) {
|
||||||
|
scenes.get(engine)?.destroy();
|
||||||
|
const picking = createPickRegistry();
|
||||||
|
const selection = createSelectionDispatcher(engine, { ...options, picking });
|
||||||
|
let destroyed = false;
|
||||||
|
const services = {
|
||||||
|
picking,
|
||||||
|
selection,
|
||||||
|
destroy() {
|
||||||
|
if (destroyed) return;
|
||||||
|
destroyed = true;
|
||||||
|
selection.destroy();
|
||||||
|
picking.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
scenes.set(engine, services);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A detached scene stays inert until an explicit new attachment replaces it. */
|
||||||
|
export function sceneServicesFor(engine) {
|
||||||
|
if (!engine) return null;
|
||||||
|
return scenes.get(engine) ?? createSceneServices(engine);
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { GROUND_HARNESSES } from '../testSupport/groundDisplayEngines.mjs';
|
||||||
|
import { createSceneServices, sceneServicesFor } from './sceneServices.js';
|
||||||
|
import { pickRegistryFor } from './pickRegistry.js';
|
||||||
|
import { selectionDispatcherFor } from './selectionDispatcher.js';
|
||||||
|
import { createAlprCamerasLayer } from '../layers/alpr/index.js';
|
||||||
|
import * as picking from './pickRegistry.js';
|
||||||
|
|
||||||
|
for (const makeHarness of GROUND_HARNESSES) {
|
||||||
|
test(`${makeHarness.name}: an enabled ALPR layer transfers pick ownership on reattachment`, (t) => {
|
||||||
|
const first = makeHarness();
|
||||||
|
const second = makeHarness();
|
||||||
|
const a = createSceneServices(first.engine);
|
||||||
|
const b = createSceneServices(second.engine);
|
||||||
|
const noop = () => {};
|
||||||
|
const layer = createAlprCamerasLayer({
|
||||||
|
source: { fetch: () => assert.fail('Reattachment must not fetch') },
|
||||||
|
services: { picking, render: { governorRequestRender: noop } },
|
||||||
|
rendererFactory({ state }) {
|
||||||
|
state.recordById.set('camera', {});
|
||||||
|
return {
|
||||||
|
init: noop,
|
||||||
|
setVisible: noop,
|
||||||
|
renderRecords: noop,
|
||||||
|
hideOnMapCredit: noop,
|
||||||
|
clearSelection: noop,
|
||||||
|
clearRendered: noop,
|
||||||
|
destroy: noop,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
t.after(() => {
|
||||||
|
layer.destroy();
|
||||||
|
a.destroy();
|
||||||
|
b.destroy();
|
||||||
|
first.destroy();
|
||||||
|
second.destroy();
|
||||||
|
});
|
||||||
|
layer.init(null, { engine: first.engine });
|
||||||
|
layer.enable();
|
||||||
|
assert.equal(a.picking.findPickOwner('camera'), 'alpr-cameras');
|
||||||
|
assert.equal(b.picking.findPickOwner('camera'), null);
|
||||||
|
layer.detachRenderer();
|
||||||
|
assert.equal(a.picking.findPickOwner('camera'), null);
|
||||||
|
layer.attachRenderer(null, { engine: second.engine });
|
||||||
|
assert.equal(b.picking.findPickOwner('camera'), 'alpr-cameras');
|
||||||
|
layer.disable();
|
||||||
|
assert.equal(b.picking.findPickOwner('camera'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`${makeHarness.name}: simultaneous scenes isolate ownership and clicks`, (t) => {
|
||||||
|
const first = makeHarness();
|
||||||
|
const second = makeHarness();
|
||||||
|
const scenes = [first, second].map(({ engine }) => ({
|
||||||
|
engine,
|
||||||
|
services: createSceneServices(engine, { keyTarget: new EventTarget() }),
|
||||||
|
}));
|
||||||
|
t.after(() => {
|
||||||
|
for (const scene of scenes) scene.services.destroy();
|
||||||
|
first.destroy();
|
||||||
|
second.destroy();
|
||||||
|
});
|
||||||
|
const calls = [[], []];
|
||||||
|
scenes.forEach(({ engine, services }, i) => {
|
||||||
|
assert.equal(pickRegistryFor(engine), services.picking);
|
||||||
|
assert.equal(selectionDispatcherFor(engine), services.selection);
|
||||||
|
services.selection.register('flights', {
|
||||||
|
owns: (id) => id === `flight-${i}`,
|
||||||
|
onSelect: (id) => calls[i].push(id),
|
||||||
|
onEmptyClick: () => calls[i].push('empty'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const [a, b] = scenes.map((scene) => scene.services);
|
||||||
|
assert.equal(a.picking.findPickOwner('flight-0'), 'flights');
|
||||||
|
assert.equal(b.picking.findPickOwner('flight-0'), null);
|
||||||
|
a.picking.registerPickOwner('cctv', (id) => id === 'camera');
|
||||||
|
assert.equal(a.picking.isOwnedByOtherLayer('flights', 'camera'), true);
|
||||||
|
assert.equal(b.picking.isOwnedByOtherLayer('flights', 'camera'), false);
|
||||||
|
first.click({ id: 'flight-0' });
|
||||||
|
second.click({ id: 'flight-1' });
|
||||||
|
first.click({ id: 'camera' });
|
||||||
|
second.click({ id: 'camera' });
|
||||||
|
assert.deepEqual(calls, [['flight-0'], ['flight-1', 'empty']]);
|
||||||
|
a.picking.unregisterPickOwner('flights');
|
||||||
|
assert.equal(b.picking.findPickOwner('flight-1'), 'flights');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`${makeHarness.name}: scene teardown and parked reattachment leave siblings live`, (t) => {
|
||||||
|
const first = makeHarness();
|
||||||
|
const second = makeHarness();
|
||||||
|
const keys = [new EventTarget(), new EventTarget()];
|
||||||
|
const a = createSceneServices(first.engine, { keyTarget: keys[0] });
|
||||||
|
const b = createSceneServices(second.engine, { keyTarget: keys[1] });
|
||||||
|
let next;
|
||||||
|
t.after(() => {
|
||||||
|
a.destroy();
|
||||||
|
b.destroy();
|
||||||
|
next?.destroy();
|
||||||
|
first.destroy();
|
||||||
|
second.destroy();
|
||||||
|
});
|
||||||
|
const calls = [];
|
||||||
|
const owner = (name) => ({
|
||||||
|
owns: (id) => id === 'shared-id',
|
||||||
|
onSelect: () => calls.push(name),
|
||||||
|
onEscape: () => calls.push(`${name}-escape`),
|
||||||
|
});
|
||||||
|
const lateRemove = a.selection.register('flights', owner('first'));
|
||||||
|
b.selection.register('flights', owner('second'));
|
||||||
|
assert.equal(first.listeners(), 4);
|
||||||
|
assert.equal(second.listeners(), 4);
|
||||||
|
a.destroy();
|
||||||
|
a.destroy();
|
||||||
|
assert.equal(first.listeners(), 0);
|
||||||
|
assert.equal(second.listeners(), 4);
|
||||||
|
assert.equal(sceneServicesFor(first.engine), a);
|
||||||
|
a.picking.registerPickOwner('late', () => true);
|
||||||
|
a.selection.register('late', owner('late'));
|
||||||
|
assert.equal(a.picking.findPickOwner('shared-id'), null);
|
||||||
|
first.click({ id: 'shared-id' });
|
||||||
|
second.click({ id: 'shared-id' });
|
||||||
|
for (const target of keys)
|
||||||
|
target.dispatchEvent(
|
||||||
|
Object.assign(new Event('keydown'), { key: 'Escape' }),
|
||||||
|
);
|
||||||
|
assert.deepEqual(calls, ['second', 'second-escape']);
|
||||||
|
next = createSceneServices(first.engine, { keyTarget: keys[0] });
|
||||||
|
next.selection.register('flights', owner('next'));
|
||||||
|
lateRemove();
|
||||||
|
a.destroy();
|
||||||
|
assert.equal(next.picking.findPickOwner('shared-id'), 'flights');
|
||||||
|
first.click({ id: 'shared-id' });
|
||||||
|
assert.deepEqual(calls, ['second', 'second-escape', 'next']);
|
||||||
|
assert.equal(b.picking.findPickOwner('shared-id'), 'flights');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { nearbyContact } from './pickRegistry.js';
|
import { sceneServicesFor } from './sceneServices.js';
|
||||||
// One selection dispatcher for the layers that draw through the globe
|
// One selection dispatcher for the layers that draw through the globe
|
||||||
// engine: one click subscription with picks
|
// engine: one click subscription with picks
|
||||||
// (engine.input.on('click', …, { pick: true, drill: 8 })), the press
|
// (engine.input.on('click', …, { pick: true, drill: 8 })), the press
|
||||||
@@ -27,16 +27,11 @@ import { nearbyContact } from './pickRegistry.js';
|
|||||||
// layer.
|
// layer.
|
||||||
|
|
||||||
import { isPointerFree as pointerFree } from './inputOwnership.js';
|
import { isPointerFree as pointerFree } from './inputOwnership.js';
|
||||||
import {
|
import { pickRegistryFor, resolvePickId } from './pickRegistry.js';
|
||||||
findPickOwner,
|
|
||||||
registerPickOwner,
|
|
||||||
resolvePickId,
|
|
||||||
unregisterPickOwner,
|
|
||||||
} from './pickRegistry.js';
|
|
||||||
import {
|
import {
|
||||||
isTrackingClickGesture,
|
isTrackingClickGesture,
|
||||||
isTrackingSelectionGesture,
|
isTrackingSelectionGesture,
|
||||||
} from './trackingClickGesture.js';
|
} from './trackingGesture.js';
|
||||||
|
|
||||||
const SELECTION_DRILL = 8;
|
const SELECTION_DRILL = 8;
|
||||||
|
|
||||||
@@ -47,11 +42,18 @@ const SELECTION_DRILL = 8;
|
|||||||
export function createSelectionDispatcher(
|
export function createSelectionDispatcher(
|
||||||
engine,
|
engine,
|
||||||
{
|
{
|
||||||
|
picking = pickRegistryFor(engine),
|
||||||
keyTarget = globalThis.document,
|
keyTarget = globalThis.document,
|
||||||
now = () => performance.now(),
|
now = () => performance.now(),
|
||||||
isPointerFree = pointerFree,
|
isPointerFree = pointerFree,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
|
const {
|
||||||
|
findPickOwner,
|
||||||
|
registerPickOwner,
|
||||||
|
unregisterPickOwner,
|
||||||
|
nearbyContact,
|
||||||
|
} = picking;
|
||||||
const owners = new Map();
|
const owners = new Map();
|
||||||
let removers = [];
|
let removers = [];
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
@@ -254,21 +256,9 @@ export function createSelectionDispatcher(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatchers = new WeakMap();
|
/** The selection dispatcher owned by the engine's attached scene. */
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
export function selectionDispatcherFor(engine) {
|
||||||
if (!engine) return null;
|
return sceneServicesFor(engine)?.selection ?? null;
|
||||||
let dispatcher = dispatchers.get(engine);
|
|
||||||
if (!dispatcher) {
|
|
||||||
dispatcher = createSelectionDispatcher(engine);
|
|
||||||
dispatchers.set(engine, dispatcher);
|
|
||||||
}
|
|
||||||
return dispatcher;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function safely(run) {
|
function safely(run) {
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ test('a click on an owned object selects it; empty space deselects everyone', ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("another layer's pick is not empty space, owned through the shared registry", () => {
|
test("another layer's pick is not empty space, owned through the shared registry", () => {
|
||||||
const { dispatcher, click } = setup({ 10: [{ id: 'cctv-1' }] });
|
const { engine, dispatcher, click } = setup({ 10: [{ id: 'cctv-1' }] });
|
||||||
registerPickOwner('cctv', (id) => id.startsWith('cctv'));
|
registerPickOwner('cctv', (id) => id.startsWith('cctv'), engine);
|
||||||
try {
|
try {
|
||||||
const flights = recorder();
|
const flights = recorder();
|
||||||
dispatcher.register('flights', {
|
dispatcher.register('flights', {
|
||||||
@@ -87,19 +87,21 @@ test("another layer's pick is not empty space, owned through the shared registry
|
|||||||
...flights.handlers,
|
...flights.handlers,
|
||||||
});
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
isOwnedByOtherLayer('cctv', 'x'),
|
isOwnedByOtherLayer('cctv', 'x', engine),
|
||||||
true,
|
true,
|
||||||
'registered for legacy handlers',
|
'registered for legacy handlers',
|
||||||
);
|
);
|
||||||
click(10);
|
click(10);
|
||||||
assert.deepEqual(flights.calls, []);
|
assert.deepEqual(flights.calls, []);
|
||||||
} finally {
|
} finally {
|
||||||
unregisterPickOwner('cctv');
|
unregisterPickOwner('cctv', engine);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a pick no layer owns is empty space, as for the old handlers', () => {
|
test('a pick no layer owns is empty space, as for the old handlers', () => {
|
||||||
const { dispatcher, click } = setup({ 10: [{ id: 'unclaimed-entity' }] });
|
const { engine, dispatcher, click } = setup({
|
||||||
|
10: [{ id: 'unclaimed-entity' }],
|
||||||
|
});
|
||||||
const flights = recorder();
|
const flights = recorder();
|
||||||
dispatcher.register('flights', {
|
dispatcher.register('flights', {
|
||||||
owns: (id) => id === 'x',
|
owns: (id) => id === 'x',
|
||||||
@@ -115,7 +117,7 @@ test('a pick no layer owns is empty space, as for the old handlers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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 { engine, dispatcher, click } = setup({
|
||||||
10: [{ id: 'trail-ae01ce', owner: 'flights-trail' }, { id: 'ae01ce' }],
|
10: [{ id: 'trail-ae01ce', owner: 'flights-trail' }, { id: 'ae01ce' }],
|
||||||
});
|
});
|
||||||
const flights = recorder();
|
const flights = recorder();
|
||||||
@@ -129,10 +131,10 @@ test('trails do not take clicks: the object under a trail is selected', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("a drill-through owner finds its object under another layer's", () => {
|
test("a drill-through owner finds its object under another layer's", () => {
|
||||||
const { dispatcher, click } = setup({
|
const { engine, dispatcher, click } = setup({
|
||||||
10: [{ id: 'cctv-1' }, { id: 'adsb:abc' }],
|
10: [{ id: 'cctv-1' }, { id: 'adsb:abc' }],
|
||||||
});
|
});
|
||||||
registerPickOwner('cctv', (id) => id.startsWith('cctv'));
|
registerPickOwner('cctv', (id) => id.startsWith('cctv'), engine);
|
||||||
try {
|
try {
|
||||||
const local = recorder();
|
const local = recorder();
|
||||||
dispatcher.register('local-adsb', {
|
dispatcher.register('local-adsb', {
|
||||||
@@ -143,12 +145,12 @@ test("a drill-through owner finds its object under another layer's", () => {
|
|||||||
click(10);
|
click(10);
|
||||||
assert.deepEqual(local.calls, [['select', 'adsb:abc', undefined]]);
|
assert.deepEqual(local.calls, [['select', 'adsb:abc', undefined]]);
|
||||||
} finally {
|
} finally {
|
||||||
unregisterPickOwner('cctv');
|
unregisterPickOwner('cctv', engine);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a drag selects nothing and deselects nothing; a long press still selects', () => {
|
test('a drag selects nothing and deselects nothing; a long press still selects', () => {
|
||||||
const { dispatcher, click } = setup({ 10: [{ id: 'ae01ce' }] });
|
const { engine, dispatcher, click } = setup({ 10: [{ id: 'ae01ce' }] });
|
||||||
const flights = recorder();
|
const flights = recorder();
|
||||||
dispatcher.register('flights', {
|
dispatcher.register('flights', {
|
||||||
owns: (id) => id === 'ae01ce',
|
owns: (id) => id === 'ae01ce',
|
||||||
@@ -189,7 +191,7 @@ test('unregister and destroy remove the subscriptions', () => {
|
|||||||
dispatcher.destroy();
|
dispatcher.destroy();
|
||||||
click(10);
|
click(10);
|
||||||
assert.deepEqual(flights.calls, []);
|
assert.deepEqual(flights.calls, []);
|
||||||
assert.equal(isOwnedByOtherLayer('other', 'a'), false);
|
assert.equal(isOwnedByOtherLayer('other', 'a', engine), false);
|
||||||
assert.ok(engine.fake.callsOf('input.on').length >= 1);
|
assert.ok(engine.fake.callsOf('input.on').length >= 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -208,7 +210,7 @@ test('the application has one dispatcher per engine', () => {
|
|||||||
|
|
||||||
test('a look-under owner (local ADS-B) keeps its own click rule beside the others', () => {
|
test('a look-under owner (local ADS-B) keeps its own click rule beside the others', () => {
|
||||||
// x: what lies under each column, top first.
|
// x: what lies under each column, top first.
|
||||||
const { dispatcher, click } = setup({
|
const { engine, dispatcher, click } = setup({
|
||||||
10: [{ id: 'local-adsb:a' }], // its own marker
|
10: [{ id: 'local-adsb:a' }], // its own marker
|
||||||
20: [{ primitive: { tileset: true } }, { id: 'local-adsb:a' }], // under a tile
|
20: [{ primitive: { tileset: true } }, { id: 'local-adsb:a' }], // under a tile
|
||||||
30: [{ id: 'gev-trail:local-adsb-head-1' }, { id: 'local-adsb:b' }], // under a trail
|
30: [{ id: 'gev-trail:local-adsb-head-1' }, { id: 'local-adsb:b' }], // under a trail
|
||||||
@@ -217,8 +219,8 @@ test('a look-under owner (local ADS-B) keeps its own click rule beside the other
|
|||||||
60: [{ id: 'cctv-1' }, { id: 'local-adsb:a' }], // a registry owner's on top
|
60: [{ id: 'cctv-1' }, { id: 'local-adsb:a' }], // a registry owner's on top
|
||||||
70: [], // empty map
|
70: [], // empty map
|
||||||
});
|
});
|
||||||
registerPickOwner('trails', (id) => id.startsWith('gev-trail:'));
|
registerPickOwner('trails', (id) => id.startsWith('gev-trail:'), engine);
|
||||||
registerPickOwner('cctv', (id) => id.startsWith('cctv'));
|
registerPickOwner('cctv', (id) => id.startsWith('cctv'), engine);
|
||||||
try {
|
try {
|
||||||
const local = recorder();
|
const local = recorder();
|
||||||
const flights = recorder();
|
const flights = recorder();
|
||||||
@@ -252,8 +254,8 @@ test('a look-under owner (local ADS-B) keeps its own click rule beside the other
|
|||||||
['empty', 0],
|
['empty', 0],
|
||||||
]);
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
unregisterPickOwner('trails');
|
unregisterPickOwner('trails', engine);
|
||||||
unregisterPickOwner('cctv');
|
unregisterPickOwner('cctv', engine);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,12 @@
|
|||||||
import { trackingEventTypes } from '../globe/cesium/screenEvents.js';
|
import { trackingEventTypes } from '../globe/cesium/screenEvents.js';
|
||||||
import { isPointerFree } from './inputOwnership.js';
|
import { isPointerFree } from './inputOwnership.js';
|
||||||
|
|
||||||
export const MAX_TRACKING_CLICK_TRAVEL_PX = 6;
|
export {
|
||||||
export const MAX_TRACKING_CLICK_DURATION_MS = 400;
|
MAX_TRACKING_CLICK_TRAVEL_PX,
|
||||||
|
MAX_TRACKING_CLICK_DURATION_MS,
|
||||||
/**
|
isTrackingSelectionGesture,
|
||||||
* Decide whether a completed press stayed spatially click-like. Duration is
|
isTrackingClickGesture,
|
||||||
* deliberately ignored so a stationary long press may still select a contact;
|
} from './trackingGesture.js';
|
||||||
* callers apply the full click classifier before destructive deselection.
|
|
||||||
* @param {{travelPx?: number}} gesture - Accumulated pointer path.
|
|
||||||
* @returns {boolean} True when travel stays within the click limit.
|
|
||||||
*/
|
|
||||||
export function isTrackingSelectionGesture(gesture = {}) {
|
|
||||||
const travelPx = Number.isFinite(gesture.travelPx)
|
|
||||||
? Math.max(0, gesture.travelPx)
|
|
||||||
: Number.POSITIVE_INFINITY;
|
|
||||||
return travelPx <= MAX_TRACKING_CLICK_TRAVEL_PX;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decide whether a completed press is a clean click suitable for deselection
|
|
||||||
* or another action that requires both short duration and low travel.
|
|
||||||
* @param {{travelPx?: number, durationMs?: number}} gesture - Accumulated path and press time.
|
|
||||||
* @returns {boolean} True when the gesture stays within both click limits.
|
|
||||||
*/
|
|
||||||
export function isTrackingClickGesture(gesture = {}) {
|
|
||||||
const durationMs = Number.isFinite(gesture.durationMs)
|
|
||||||
? Math.max(0, gesture.durationMs)
|
|
||||||
: Number.POSITIVE_INFINITY;
|
|
||||||
return (
|
|
||||||
isTrackingSelectionGesture(gesture) &&
|
|
||||||
durationMs <= MAX_TRACKING_CLICK_DURATION_MS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind LEFT_DOWN/MOUSE_MOVE/LEFT_UP accounting ahead of a scene click.
|
* Bind LEFT_DOWN/MOUSE_MOVE/LEFT_UP accounting ahead of a scene click.
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
export const MAX_TRACKING_CLICK_TRAVEL_PX = 6;
|
||||||
|
export const MAX_TRACKING_CLICK_DURATION_MS = 400;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide whether a completed press stayed spatially click-like. Duration is
|
||||||
|
* deliberately ignored so a stationary long press may still select a contact;
|
||||||
|
* callers apply the full click classifier before destructive deselection.
|
||||||
|
* @param {{travelPx?: number}} gesture - Accumulated pointer path.
|
||||||
|
* @returns {boolean} True when travel stays within the click limit.
|
||||||
|
*/
|
||||||
|
export function isTrackingSelectionGesture(gesture = {}) {
|
||||||
|
const travelPx = Number.isFinite(gesture.travelPx)
|
||||||
|
? Math.max(0, gesture.travelPx)
|
||||||
|
: Number.POSITIVE_INFINITY;
|
||||||
|
return travelPx <= MAX_TRACKING_CLICK_TRAVEL_PX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide whether a completed press is a clean click suitable for deselection
|
||||||
|
* or another action that requires both short duration and low travel.
|
||||||
|
* @param {{travelPx?: number, durationMs?: number}} gesture - Accumulated path and press time.
|
||||||
|
* @returns {boolean} True when the gesture stays within both click limits.
|
||||||
|
*/
|
||||||
|
export function isTrackingClickGesture(gesture = {}) {
|
||||||
|
const durationMs = Number.isFinite(gesture.durationMs)
|
||||||
|
? Math.max(0, gesture.durationMs)
|
||||||
|
: Number.POSITIVE_INFINITY;
|
||||||
|
return (
|
||||||
|
isTrackingSelectionGesture(gesture) &&
|
||||||
|
durationMs <= MAX_TRACKING_CLICK_DURATION_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ export function createAircraftSceneBinding({
|
|||||||
let resumeOrigin = 'programmatic';
|
let resumeOrigin = 'programmatic';
|
||||||
function removeSceneListeners() {
|
function removeSceneListeners() {
|
||||||
parts.tracking._removeClickHandler();
|
parts.tracking._removeClickHandler();
|
||||||
services.picking.unregisterPickOwner(id);
|
services.picking.unregisterPickOwner(id, flightState._renderer?.engine);
|
||||||
if (flightState._preRenderRemove) {
|
if (flightState._preRenderRemove) {
|
||||||
flightState._preRenderRemove();
|
flightState._preRenderRemove();
|
||||||
flightState._preRenderRemove = null;
|
flightState._preRenderRemove = null;
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export function createAlprCamerasLayer({
|
|||||||
renderSaturated: false,
|
renderSaturated: false,
|
||||||
};
|
};
|
||||||
let renderer = null;
|
let renderer = null;
|
||||||
|
let sceneEngine = null;
|
||||||
let restoringView = false;
|
let restoringView = false;
|
||||||
|
|
||||||
function setAlprStatus(status, error = null) {
|
function setAlprStatus(status, error = null) {
|
||||||
@@ -280,6 +281,7 @@ export function createAlprCamerasLayer({
|
|||||||
if (renderer?.ready) throw new Error('ALPR layer is already initialized');
|
if (renderer?.ready) throw new Error('ALPR layer is already initialized');
|
||||||
if (typeof rendererFactory !== 'function')
|
if (typeof rendererFactory !== 'function')
|
||||||
throw new TypeError('An ALPR renderer factory is required');
|
throw new TypeError('An ALPR renderer factory is required');
|
||||||
|
sceneEngine = options.engine;
|
||||||
renderer = rendererFactory({
|
renderer = rendererFactory({
|
||||||
state,
|
state,
|
||||||
services,
|
services,
|
||||||
@@ -288,15 +290,22 @@ export function createAlprCamerasLayer({
|
|||||||
});
|
});
|
||||||
renderer.init(viewer, { onMove: scheduleLoad });
|
renderer.init(viewer, { onMove: scheduleLoad });
|
||||||
if (state.enabled) {
|
if (state.enabled) {
|
||||||
|
registerPickOwner(
|
||||||
|
LAYER_ID,
|
||||||
|
(id) => state.recordById.has(id),
|
||||||
|
sceneEngine,
|
||||||
|
);
|
||||||
renderer.setVisible(true);
|
renderer.setVisible(true);
|
||||||
if (!restoringView) renderer.renderRecords();
|
if (!restoringView) renderer.renderRecords();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
detachRenderer() {
|
detachRenderer() {
|
||||||
|
unregisterPickOwner(LAYER_ID, sceneEngine);
|
||||||
restoringView = true;
|
restoringView = true;
|
||||||
clearTimeout(state.debounceTimer);
|
clearTimeout(state.debounceTimer);
|
||||||
renderer?.destroy({ retainContext: true });
|
renderer?.destroy({ retainContext: true });
|
||||||
renderer = null;
|
renderer = null;
|
||||||
|
sceneEngine = null;
|
||||||
},
|
},
|
||||||
resumeViewport() {
|
resumeViewport() {
|
||||||
if (!restoringView) return;
|
if (!restoringView) return;
|
||||||
@@ -309,14 +318,18 @@ export function createAlprCamerasLayer({
|
|||||||
enable() {
|
enable() {
|
||||||
if (state.enabled) return;
|
if (state.enabled) return;
|
||||||
state.enabled = true;
|
state.enabled = true;
|
||||||
registerPickOwner(LAYER_ID, (id) => state.recordById.has(id));
|
registerPickOwner(
|
||||||
|
LAYER_ID,
|
||||||
|
(id) => state.recordById.has(id),
|
||||||
|
sceneEngine,
|
||||||
|
);
|
||||||
renderer.setVisible(true);
|
renderer.setVisible(true);
|
||||||
// DataLayerManager calls update() right after enable(); it owns the first fetch.
|
// DataLayerManager calls update() right after enable(); it owns the first fetch.
|
||||||
},
|
},
|
||||||
disable() {
|
disable() {
|
||||||
state.enabled = false;
|
state.enabled = false;
|
||||||
renderer?.hideOnMapCredit();
|
renderer?.hideOnMapCredit();
|
||||||
unregisterPickOwner(LAYER_ID);
|
unregisterPickOwner(LAYER_ID, sceneEngine);
|
||||||
clearUnavailableRetry();
|
clearUnavailableRetry();
|
||||||
clearTimeout(state.debounceTimer);
|
clearTimeout(state.debounceTimer);
|
||||||
state.abort?.abort();
|
state.abort?.abort();
|
||||||
@@ -337,6 +350,7 @@ export function createAlprCamerasLayer({
|
|||||||
this.disable();
|
this.disable();
|
||||||
renderer?.destroy();
|
renderer?.destroy();
|
||||||
renderer = null;
|
renderer = null;
|
||||||
|
sceneEngine = null;
|
||||||
state.records = [];
|
state.records = [];
|
||||||
state.recordById = new Map();
|
state.recordById = new Map();
|
||||||
state.lastQueryBox = null;
|
state.lastQueryBox = null;
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ export function createBikeshareDisplay(engine, context) {
|
|||||||
}
|
}
|
||||||
function releaseInput() {
|
function releaseInput() {
|
||||||
parts.selection._uninstallClickHandler();
|
parts.selection._uninstallClickHandler();
|
||||||
unregisterPickOwner?.(LAYER_ID);
|
unregisterPickOwner?.(LAYER_ID, engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
const methods = {
|
const methods = {
|
||||||
@@ -336,8 +336,10 @@ export function createBikeshareDisplay(engine, context) {
|
|||||||
state._overlayHost.setVisible(BIKESHARE_SELECTED_OVERLAY_SOURCE_ID, true);
|
state._overlayHost.setVisible(BIKESHARE_SELECTED_OVERLAY_SOURCE_ID, true);
|
||||||
parts.selection._installClickHandler();
|
parts.selection._installClickHandler();
|
||||||
// Pick ownership: station point ids are render-map keys.
|
// Pick ownership: station point ids are render-map keys.
|
||||||
registerPickOwner?.(LAYER_ID, (pickedId) =>
|
registerPickOwner?.(
|
||||||
state._stationRenderMap.has(pickedId),
|
LAYER_ID,
|
||||||
|
(pickedId) => state._stationRenderMap.has(pickedId),
|
||||||
|
engine,
|
||||||
);
|
);
|
||||||
attachCamera();
|
attachCamera();
|
||||||
void parts.viewport.runProximityCheck();
|
void parts.viewport.runProximityCheck();
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ export function createLifecycle({
|
|||||||
if (!parts.renderer) return;
|
if (!parts.renderer) return;
|
||||||
layerState._restoringViewport = true;
|
layerState._restoringViewport = true;
|
||||||
parts.renderer.hideCredit();
|
parts.renderer.hideCredit();
|
||||||
unregisterPickOwner('cctv');
|
unregisterPickOwner('cctv', parts.renderer?.engine);
|
||||||
if (layerState._mapStackListener) {
|
if (layerState._mapStackListener) {
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
'gev:map-stack-changed',
|
'gev:map-stack-changed',
|
||||||
@@ -528,19 +528,23 @@ export function createLifecycle({
|
|||||||
parts.renderer?.showCredit();
|
parts.renderer?.showCredit();
|
||||||
// Pick-ownership (H2): camera billboards use the camera id directly;
|
// Pick-ownership (H2): camera billboards use the camera id directly;
|
||||||
// coverage polyline entities use `cctv-<cameraId>-<role>` entity ids.
|
// coverage polyline entities use `cctv-<cameraId>-<role>` entity ids.
|
||||||
registerPickOwner('cctv', (pickedId) => {
|
registerPickOwner(
|
||||||
if (layerState._recordById.has(pickedId)) return true;
|
'cctv',
|
||||||
if (
|
(pickedId) => {
|
||||||
typeof pickedId === 'string' &&
|
if (layerState._recordById.has(pickedId)) return true;
|
||||||
pickedId.startsWith(GIZMO_ID_PREFIX)
|
if (
|
||||||
)
|
typeof pickedId === 'string' &&
|
||||||
return true;
|
pickedId.startsWith(GIZMO_ID_PREFIX)
|
||||||
const coverage =
|
)
|
||||||
/^cctv-(.+)-(?:ray-tl|ray-tr|ray-br|ray-bl|cap|plane|plane-label)$/.exec(
|
return true;
|
||||||
pickedId,
|
const coverage =
|
||||||
);
|
/^cctv-(.+)-(?:ray-tl|ray-tr|ray-br|ray-bl|cap|plane|plane-label)$/.exec(
|
||||||
return Boolean(coverage && layerState._recordById.has(coverage[1]));
|
pickedId,
|
||||||
});
|
);
|
||||||
|
return Boolean(coverage && layerState._recordById.has(coverage[1]));
|
||||||
|
},
|
||||||
|
parts.renderer?.engine,
|
||||||
|
);
|
||||||
const activeRecord = parts.selection.getActiveRecord();
|
const activeRecord = parts.selection.getActiveRecord();
|
||||||
if (activeRecord) {
|
if (activeRecord) {
|
||||||
parts.projection.ensureProjectionRuntime(activeRecord);
|
parts.projection.ensureProjectionRuntime(activeRecord);
|
||||||
@@ -573,7 +577,7 @@ export function createLifecycle({
|
|||||||
disable() {
|
disable() {
|
||||||
parts.renderer?.hideCredit();
|
parts.renderer?.hideCredit();
|
||||||
layerState._enabled = false;
|
layerState._enabled = false;
|
||||||
unregisterPickOwner('cctv');
|
unregisterPickOwner('cctv', parts.renderer?.engine);
|
||||||
// ADJUST mode does not survive a layer toggle — predictable re-entry.
|
// ADJUST mode does not survive a layer toggle — predictable re-entry.
|
||||||
layerState._calibrationMode = false;
|
layerState._calibrationMode = false;
|
||||||
releaseContinuousRender('cctv-adjust');
|
releaseContinuousRender('cctv-adjust');
|
||||||
@@ -606,7 +610,7 @@ export function createLifecycle({
|
|||||||
'visibilitychange',
|
'visibilitychange',
|
||||||
parts.cards.handleVisibilityChange,
|
parts.cards.handleVisibilityChange,
|
||||||
);
|
);
|
||||||
unregisterPickOwner('cctv');
|
unregisterPickOwner('cctv', parts.renderer?.engine);
|
||||||
if (layerState._mapStackListener && typeof window !== 'undefined') {
|
if (layerState._mapStackListener && typeof window !== 'undefined') {
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
'gev:map-stack-changed',
|
'gev:map-stack-changed',
|
||||||
|
|||||||
@@ -590,6 +590,7 @@ export function createCctvDisplay(engine, { state, services = {} } = {}) {
|
|||||||
let credit = false;
|
let credit = false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
engine,
|
||||||
attach() {
|
attach() {
|
||||||
if (destroyed) throw new Error('CCTV display is destroyed');
|
if (destroyed) throw new Error('CCTV display is destroyed');
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|||||||
@@ -244,6 +244,13 @@ export function createCyclonesLayer({
|
|||||||
removeClickCapture = null;
|
removeClickCapture = null;
|
||||||
owner?.destroy();
|
owner?.destroy();
|
||||||
}
|
}
|
||||||
|
function registerOwnership() {
|
||||||
|
registerPickOwner(
|
||||||
|
'weather-cyclones',
|
||||||
|
(id) => enabled && rendering?.ownsPickId?.(id) === true,
|
||||||
|
engine,
|
||||||
|
);
|
||||||
|
}
|
||||||
const layer = {
|
const layer = {
|
||||||
id: 'weather-cyclones',
|
id: 'weather-cyclones',
|
||||||
name: 'Cyclone advisories',
|
name: 'Cyclone advisories',
|
||||||
@@ -260,6 +267,7 @@ export function createCyclonesLayer({
|
|||||||
? createRendering(engine, { overlayHost })
|
? createRendering(engine, { overlayHost })
|
||||||
: null;
|
: null;
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
|
registerOwnership();
|
||||||
installSelection();
|
installSelection();
|
||||||
if (rendering && snapshot && !snapshot.unavailable) {
|
if (rendering && snapshot && !snapshot.unavailable) {
|
||||||
const active = rendering;
|
const active = rendering;
|
||||||
@@ -269,6 +277,7 @@ export function createCyclonesLayer({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
detachRenderer() {
|
detachRenderer() {
|
||||||
|
unregisterPickOwner('weather-cyclones', engine);
|
||||||
removeSelection();
|
removeSelection();
|
||||||
++navigationGeneration;
|
++navigationGeneration;
|
||||||
rendering?.destroy();
|
rendering?.destroy();
|
||||||
@@ -286,16 +295,13 @@ export function createCyclonesLayer({
|
|||||||
enable() {
|
enable() {
|
||||||
if (!destroyed && !enabled) {
|
if (!destroyed && !enabled) {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
registerPickOwner(
|
registerOwnership();
|
||||||
'weather-cyclones',
|
|
||||||
(id) => enabled && rendering?.ownsPickId?.(id) === true,
|
|
||||||
);
|
|
||||||
installSelection();
|
installSelection();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
disable() {
|
disable() {
|
||||||
enabled = false;
|
enabled = false;
|
||||||
unregisterPickOwner('weather-cyclones');
|
unregisterPickOwner('weather-cyclones', engine);
|
||||||
removeSelection();
|
removeSelection();
|
||||||
request?.abort();
|
request?.abort();
|
||||||
request = null;
|
request = null;
|
||||||
|
|||||||
@@ -105,13 +105,16 @@ test('map selection yields to pointer owners and owns only its enabled handler',
|
|||||||
layer.enable();
|
layer.enable();
|
||||||
layer.enable();
|
layer.enable();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
isOwnedByOtherLayer('flights', 'cyclone:ep162026:center'),
|
isOwnedByOtherLayer('flights', 'cyclone:ep162026:center', h.engine),
|
||||||
true,
|
true,
|
||||||
'flight tracking recognizes the cyclone pick and leaves the camera alone',
|
'flight tracking recognizes the cyclone pick and leaves the camera alone',
|
||||||
);
|
);
|
||||||
assert.equal(isOwnedByOtherLayer('vessels', 'cyclone:ep162026:center'), true);
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
isOwnedByOtherLayer('flights', 'cyclone:ep162026:foreign'),
|
isOwnedByOtherLayer('vessels', 'cyclone:ep162026:center', h.engine),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isOwnedByOtherLayer('flights', 'cyclone:ep162026:foreign', h.engine),
|
||||||
false,
|
false,
|
||||||
'registration recognizes exact current IDs, not a prefix',
|
'registration recognizes exact current IDs, not a prefix',
|
||||||
);
|
);
|
||||||
@@ -222,7 +225,7 @@ test('map selection yields to pointer owners and owns only its enabled handler',
|
|||||||
);
|
);
|
||||||
layer.disable();
|
layer.disable();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
isOwnedByOtherLayer('flights', 'cyclone:ep162026:center'),
|
isOwnedByOtherLayer('flights', 'cyclone:ep162026:center', h.engine),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
assert.equal(layer.getDiagnostics().selectionActive, false);
|
assert.equal(layer.getDiagnostics().selectionActive, false);
|
||||||
@@ -238,7 +241,10 @@ test('map selection yields to pointer owners and owns only its enabled handler',
|
|||||||
assert.equal(h.picks, afterDisable, 'disable removes the click listener');
|
assert.equal(h.picks, afterDisable, 'disable removes the click listener');
|
||||||
layer.enable();
|
layer.enable();
|
||||||
await layer.update();
|
await layer.update();
|
||||||
assert.equal(isOwnedByOtherLayer('flights', 'cyclone:ep162026:center'), true);
|
assert.equal(
|
||||||
|
isOwnedByOtherLayer('flights', 'cyclone:ep162026:center', h.engine),
|
||||||
|
true,
|
||||||
|
);
|
||||||
h.click(click);
|
h.click(click);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
h.picks,
|
h.picks,
|
||||||
@@ -248,7 +254,7 @@ test('map selection yields to pointer owners and owns only its enabled handler',
|
|||||||
assert.equal(h.subscriptions(), 2);
|
assert.equal(h.subscriptions(), 2);
|
||||||
layer.destroy();
|
layer.destroy();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
isOwnedByOtherLayer('flights', 'cyclone:ep162026:center'),
|
isOwnedByOtherLayer('flights', 'cyclone:ep162026:center', h.engine),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
const afterDestroy = h.picks;
|
const afterDestroy = h.picks;
|
||||||
@@ -894,12 +900,15 @@ test('advisories delivered between scenes keep a deliberate selection without an
|
|||||||
const pending = deferred();
|
const pending = deferred();
|
||||||
let reads = 0;
|
let reads = 0;
|
||||||
const drawn = [];
|
const drawn = [];
|
||||||
|
const first = createFakeEngine();
|
||||||
|
const second = createFakeEngine();
|
||||||
const layer = createCyclonesLayer({
|
const layer = createCyclonesLayer({
|
||||||
feed: {
|
feed: {
|
||||||
getSnapshot: () =>
|
getSnapshot: () =>
|
||||||
++reads === 1 ? snapshot([storm()]) : pending.promise,
|
++reads === 1 ? snapshot([storm()]) : pending.promise,
|
||||||
},
|
},
|
||||||
createRendering: () => ({
|
createRendering: () => ({
|
||||||
|
ownsPickId: (id) => id === 'cyclone:ep152026:center',
|
||||||
setSnapshot(next) {
|
setSnapshot(next) {
|
||||||
drawn.push(next.storms.map((s) => s.advisoryNumber));
|
drawn.push(next.storms.map((s) => s.advisoryNumber));
|
||||||
return true;
|
return true;
|
||||||
@@ -909,17 +918,39 @@ test('advisories delivered between scenes keep a deliberate selection without an
|
|||||||
destroy() {},
|
destroy() {},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
layer.init(null, { engine: createFakeEngine() });
|
layer.init(null, { engine: first });
|
||||||
layer.enable();
|
layer.enable();
|
||||||
|
assert.equal(
|
||||||
|
isOwnedByOtherLayer('flights', 'cyclone:ep152026:center', first),
|
||||||
|
true,
|
||||||
|
);
|
||||||
await layer.update();
|
await layer.update();
|
||||||
layer.setParams({ stormId: 'ep152026' });
|
layer.setParams({ stormId: 'ep152026' });
|
||||||
const refresh = layer.update();
|
const refresh = layer.update();
|
||||||
layer.detachRenderer();
|
layer.detachRenderer();
|
||||||
|
assert.equal(
|
||||||
|
isOwnedByOtherLayer('flights', 'cyclone:ep152026:center', first),
|
||||||
|
false,
|
||||||
|
);
|
||||||
pending.resolve(snapshot([{ ...storm(), advisoryNumber: '11' }]));
|
pending.resolve(snapshot([{ ...storm(), advisoryNumber: '11' }]));
|
||||||
await refresh;
|
await refresh;
|
||||||
assert.equal(layer.getDiagnostics().selectedId, 'ep152026');
|
assert.equal(layer.getDiagnostics().selectedId, 'ep152026');
|
||||||
await layer.attachRenderer(null, { engine: createFakeEngine() });
|
await layer.attachRenderer(null, { engine: second });
|
||||||
|
assert.equal(
|
||||||
|
isOwnedByOtherLayer('flights', 'cyclone:ep152026:center', second),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isOwnedByOtherLayer('flights', 'cyclone:ep152026:center', first),
|
||||||
|
false,
|
||||||
|
);
|
||||||
assert.deepEqual(drawn, [['10'], ['11']]);
|
assert.deepEqual(drawn, [['10'], ['11']]);
|
||||||
assert.equal(reads, 2);
|
assert.equal(reads, 2);
|
||||||
layer.destroy();
|
layer.destroy();
|
||||||
|
assert.equal(
|
||||||
|
isOwnedByOtherLayer('flights', 'cyclone:ep152026:center', second),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
first.destroy();
|
||||||
|
second.destroy();
|
||||||
});
|
});
|
||||||
@@ -1537,7 +1537,7 @@ export function createDirectionsLayer({
|
|||||||
clearRouteGraphics();
|
clearRouteGraphics();
|
||||||
_selectedStep = selected;
|
_selectedStep = selected;
|
||||||
_removeClickHandler();
|
_removeClickHandler();
|
||||||
services.picking.unregisterPickOwner('directions');
|
services.picking.unregisterPickOwner('directions', _engine);
|
||||||
_display?.destroy();
|
_display?.destroy();
|
||||||
_overlayHost.setVisible(DIRECTIONS_STEP_OVERLAY_SOURCE_ID, false);
|
_overlayHost.setVisible(DIRECTIONS_STEP_OVERLAY_SOURCE_ID, false);
|
||||||
_display = _engine = null;
|
_display = _engine = null;
|
||||||
@@ -1554,6 +1554,7 @@ export function createDirectionsLayer({
|
|||||||
'directions',
|
'directions',
|
||||||
(pickedId) =>
|
(pickedId) =>
|
||||||
typeof pickedId === 'string' && pickedId.startsWith('directions:'),
|
typeof pickedId === 'string' && pickedId.startsWith('directions:'),
|
||||||
|
_engine,
|
||||||
);
|
);
|
||||||
syncRenderHold();
|
syncRenderHold();
|
||||||
},
|
},
|
||||||
@@ -1564,7 +1565,7 @@ export function createDirectionsLayer({
|
|||||||
clearAll();
|
clearAll();
|
||||||
_overlayHost.setVisible(DIRECTIONS_STEP_OVERLAY_SOURCE_ID, false);
|
_overlayHost.setVisible(DIRECTIONS_STEP_OVERLAY_SOURCE_ID, false);
|
||||||
_removeClickHandler();
|
_removeClickHandler();
|
||||||
services.picking.unregisterPickOwner('directions');
|
services.picking.unregisterPickOwner('directions', _engine);
|
||||||
_display?.show(false);
|
_display?.show(false);
|
||||||
syncRenderHold();
|
syncRenderHold();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function createDroneAirspaceLayer({
|
|||||||
throw new TypeError('Drone airspace requires a snapshot source');
|
throw new TypeError('Drone airspace requires a snapshot source');
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let renderer = null;
|
let renderer = null;
|
||||||
|
let sceneEngine = null;
|
||||||
/** The rows drawn, kept for the next globe's renderer. */
|
/** The rows drawn, kept for the next globe's renderer. */
|
||||||
let _rows = [];
|
let _rows = [];
|
||||||
let _request = null;
|
let _request = null;
|
||||||
@@ -119,7 +120,11 @@ export function createDroneAirspaceLayer({
|
|||||||
}
|
}
|
||||||
if (picked) {
|
if (picked) {
|
||||||
const pickId = picking.resolvePickId(picked);
|
const pickId = picking.resolvePickId(picked);
|
||||||
if (pickId && picking.isOwnedByOtherLayer(layer.id, pickId)) return;
|
if (
|
||||||
|
pickId &&
|
||||||
|
picking.isOwnedByOtherLayer(layer.id, pickId, sceneEngine)
|
||||||
|
)
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (_selectedId) {
|
if (_selectedId) {
|
||||||
_selectedId = null;
|
_selectedId = null;
|
||||||
@@ -168,6 +173,7 @@ export function createDroneAirspaceLayer({
|
|||||||
attachRenderer(viewer, { engine = null } = {}) {
|
attachRenderer(viewer, { engine = null } = {}) {
|
||||||
if (!initialized || renderer)
|
if (!initialized || renderer)
|
||||||
throw new Error('Invalid drone airspace scene attachment');
|
throw new Error('Invalid drone airspace scene attachment');
|
||||||
|
sceneEngine = engine;
|
||||||
renderer = createRenderer({ engine });
|
renderer = createRenderer({ engine });
|
||||||
renderer.setVisible(_enabled);
|
renderer.setVisible(_enabled);
|
||||||
if (_rows.length) renderer.replace(_rows);
|
if (_rows.length) renderer.replace(_rows);
|
||||||
@@ -185,6 +191,7 @@ export function createDroneAirspaceLayer({
|
|||||||
overlayHost?.setVisible?.(DRONE_AIRSPACE_OVERLAY_SOURCE_ID, false);
|
overlayHost?.setVisible?.(DRONE_AIRSPACE_OVERLAY_SOURCE_ID, false);
|
||||||
renderer?.destroy();
|
renderer?.destroy();
|
||||||
renderer = null;
|
renderer = null;
|
||||||
|
sceneEngine = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function createLifecycle({
|
|||||||
components.selection.removeClickHandler();
|
components.selection.removeClickHandler();
|
||||||
components.viewport.removeMoveEndWatcher();
|
components.viewport.removeMoveEndWatcher();
|
||||||
components.viewport.removeLodWatcher();
|
components.viewport.removeLodWatcher();
|
||||||
unregisterPickOwner(id);
|
unregisterPickOwner(id, layerState._renderer?.engine);
|
||||||
components.rendering.removeDetectionCollections();
|
components.rendering.removeDetectionCollections();
|
||||||
releaseHeat();
|
releaseHeat();
|
||||||
layerState._renderer?.destroy();
|
layerState._renderer?.destroy();
|
||||||
@@ -89,8 +89,10 @@ export function createLifecycle({
|
|||||||
components.viewport.installLodWatcher();
|
components.viewport.installLodWatcher();
|
||||||
components.viewport.installMoveEndWatcher();
|
components.viewport.installMoveEndWatcher();
|
||||||
components.selection.installClickHandler();
|
components.selection.installClickHandler();
|
||||||
registerPickOwner(id, (pickedId) =>
|
registerPickOwner(
|
||||||
layerState._pickIndexById.has(pickedId),
|
id,
|
||||||
|
(pickedId) => layerState._pickIndexById.has(pickedId),
|
||||||
|
layerState._renderer?.engine,
|
||||||
);
|
);
|
||||||
components.rendering.renderCurrentLod(true);
|
components.rendering.renderCurrentLod(true);
|
||||||
},
|
},
|
||||||
@@ -117,7 +119,7 @@ export function createLifecycle({
|
|||||||
clearSelectedEntityContextForLayer(id);
|
clearSelectedEntityContextForLayer(id);
|
||||||
components.selection.removeClickHandler();
|
components.selection.removeClickHandler();
|
||||||
components.viewport.removeMoveEndWatcher();
|
components.viewport.removeMoveEndWatcher();
|
||||||
unregisterPickOwner(id);
|
unregisterPickOwner(id, layerState._renderer?.engine);
|
||||||
components.viewport.removeLodWatcher();
|
components.viewport.removeLodWatcher();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -131,7 +133,7 @@ export function createLifecycle({
|
|||||||
components.viewport.removeLodWatcher();
|
components.viewport.removeLodWatcher();
|
||||||
components.viewport.removeMoveEndWatcher();
|
components.viewport.removeMoveEndWatcher();
|
||||||
components.selection.removeClickHandler();
|
components.selection.removeClickHandler();
|
||||||
unregisterPickOwner(id);
|
unregisterPickOwner(id, layerState._renderer?.engine);
|
||||||
components.rendering.removeDetectionCollections();
|
components.rendering.removeDetectionCollections();
|
||||||
layerState._renderer?.destroy();
|
layerState._renderer?.destroy();
|
||||||
layerState._renderer = null;
|
layerState._renderer = null;
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export function createFirmsDisplay(
|
|||||||
sprites = null;
|
sprites = null;
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
|
engine,
|
||||||
view: layerView.view,
|
view: layerView.view,
|
||||||
dataSource,
|
dataSource,
|
||||||
project: (position, result) =>
|
project: (position, result) =>
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ export function createSelection({
|
|||||||
// "empty space" — leave the selection alone and let that layer handle it.
|
// "empty space" — leave the selection alone and let that layer handle it.
|
||||||
if (picked) {
|
if (picked) {
|
||||||
const pickedId = resolvePickId(picked);
|
const pickedId = resolvePickId(picked);
|
||||||
if (pickedId && isOwnedByOtherLayer(id, pickedId)) return;
|
if (
|
||||||
|
pickedId &&
|
||||||
|
isOwnedByOtherLayer(id, pickedId, layerState._renderer?.engine)
|
||||||
|
)
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const cardHit = overlayHost.hitTest?.(
|
const cardHit = overlayHost.hitTest?.(
|
||||||
click.position?.x,
|
click.position?.x,
|
||||||
|
|||||||
@@ -135,8 +135,10 @@ export function createLifecycle({
|
|||||||
}
|
}
|
||||||
if (!renderer) return;
|
if (!renderer) return;
|
||||||
parts.tracking._installClickHandler();
|
parts.tracking._installClickHandler();
|
||||||
registerPickOwner('flights', (pickedId) =>
|
registerPickOwner(
|
||||||
flightState._billboards.has(pickedId),
|
'flights',
|
||||||
|
(pickedId) => flightState._billboards.has(pickedId),
|
||||||
|
flightState._renderer?.engine,
|
||||||
);
|
);
|
||||||
// Force a fresh rotation pass on the first tick after re-enable
|
// Force a fresh rotation pass on the first tick after re-enable
|
||||||
flightState._lastCamPoseSig = '';
|
flightState._lastCamPoseSig = '';
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export function createGhostMapsLayer({
|
|||||||
if (typeof source?.getCatalog !== 'function')
|
if (typeof source?.getCatalog !== 'function')
|
||||||
throw new TypeError('GhostMaps layer requires a catalog source');
|
throw new TypeError('GhostMaps layer requires a catalog source');
|
||||||
let _display = null;
|
let _display = null;
|
||||||
|
let sceneEngine = null;
|
||||||
let _enabled = false;
|
let _enabled = false;
|
||||||
let _catalog = null;
|
let _catalog = null;
|
||||||
let _lastError = null;
|
let _lastError = null;
|
||||||
@@ -538,6 +539,7 @@ export function createGhostMapsLayer({
|
|||||||
GHOSTMAPS_LAYER_ID,
|
GHOSTMAPS_LAYER_ID,
|
||||||
(pickedId) =>
|
(pickedId) =>
|
||||||
typeof pickedId === 'string' && pickedId.startsWith(PICK_PREFIX),
|
typeof pickedId === 'string' && pickedId.startsWith(PICK_PREFIX),
|
||||||
|
sceneEngine,
|
||||||
);
|
);
|
||||||
_clickHandler = _display.onClick((click, pick) => {
|
_clickHandler = _display.onClick((click, pick) => {
|
||||||
if (pointer && !pointer.isPointerFree()) return;
|
if (pointer && !pointer.isPointerFree()) return;
|
||||||
@@ -560,7 +562,10 @@ export function createGhostMapsLayer({
|
|||||||
}
|
}
|
||||||
if (picked) {
|
if (picked) {
|
||||||
const pickId = picking.resolvePickId(picked);
|
const pickId = picking.resolvePickId(picked);
|
||||||
if (pickId && picking.isOwnedByOtherLayer(GHOSTMAPS_LAYER_ID, pickId))
|
if (
|
||||||
|
pickId &&
|
||||||
|
picking.isOwnedByOtherLayer(GHOSTMAPS_LAYER_ID, pickId, sceneEngine)
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_selectedKey) {
|
if (_selectedKey) {
|
||||||
@@ -573,7 +578,7 @@ export function createGhostMapsLayer({
|
|||||||
function removeClickHandler() {
|
function removeClickHandler() {
|
||||||
_clickHandler?.();
|
_clickHandler?.();
|
||||||
_clickHandler = null;
|
_clickHandler = null;
|
||||||
picking?.unregisterPickOwner?.(GHOSTMAPS_LAYER_ID);
|
picking?.unregisterPickOwner?.(GHOSTMAPS_LAYER_ID, sceneEngine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function featureCount() {
|
function featureCount() {
|
||||||
@@ -599,6 +604,7 @@ export function createGhostMapsLayer({
|
|||||||
if (_display) throw new Error('GhostMaps display is already attached');
|
if (_display) throw new Error('GhostMaps display is already attached');
|
||||||
if (typeof createRenderer !== 'function')
|
if (typeof createRenderer !== 'function')
|
||||||
throw new TypeError('GhostMaps needs a renderer factory');
|
throw new TypeError('GhostMaps needs a renderer factory');
|
||||||
|
sceneEngine = engine;
|
||||||
_display = createRenderer(engine);
|
_display = createRenderer(engine);
|
||||||
if (!_enabled) return;
|
if (!_enabled) return;
|
||||||
installClickHandler();
|
installClickHandler();
|
||||||
@@ -629,6 +635,7 @@ export function createGhostMapsLayer({
|
|||||||
overlayHost?.clearSource?.(GHOSTMAPS_OVERLAY_SOURCE_ID);
|
overlayHost?.clearSource?.(GHOSTMAPS_OVERLAY_SOURCE_ID);
|
||||||
_display?.destroy();
|
_display?.destroy();
|
||||||
_display = null;
|
_display = null;
|
||||||
|
sceneEngine = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
|
|||||||
@@ -620,8 +620,10 @@ export function createInstallationsDisplay(
|
|||||||
namedMarkers.enable();
|
namedMarkers.enable();
|
||||||
namedMarkers.sync(state.records || []);
|
namedMarkers.sync(state.records || []);
|
||||||
claimCameraSensitivity(engine, LAYER_ID, 0.01);
|
claimCameraSensitivity(engine, LAYER_ID, 0.01);
|
||||||
services.picking.registerPickOwner(LAYER_ID, (id) =>
|
services.picking.registerPickOwner(
|
||||||
state.recordById.has(id),
|
LAYER_ID,
|
||||||
|
(id) => state.recordById.has(id),
|
||||||
|
engine,
|
||||||
);
|
);
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
// The layer manager calls update() right after enable(), which owns
|
// The layer manager calls update() right after enable(), which owns
|
||||||
@@ -639,7 +641,7 @@ export function createInstallationsDisplay(
|
|||||||
state.coverage = { kind: 'viewport' };
|
state.coverage = { kind: 'viewport' };
|
||||||
state.cameraLoadKey = null;
|
state.cameraLoadKey = null;
|
||||||
state.cameraLoadOwner = null;
|
state.cameraLoadOwner = null;
|
||||||
services.picking.unregisterPickOwner(LAYER_ID);
|
services.picking.unregisterPickOwner(LAYER_ID, engine);
|
||||||
parts.viewport.clearUnavailableRetry();
|
parts.viewport.clearUnavailableRetry();
|
||||||
clearTimeout(state.timer);
|
clearTimeout(state.timer);
|
||||||
state.abort?.abort();
|
state.abort?.abort();
|
||||||
@@ -664,7 +666,7 @@ export function createInstallationsDisplay(
|
|||||||
detach() {
|
detach() {
|
||||||
services.credits?.hideOsmCredit?.(engine, LAYER_ID);
|
services.credits?.hideOsmCredit?.(engine, LAYER_ID);
|
||||||
services.credits?.hideGooglePlacesCredit?.(engine, LAYER_ID);
|
services.credits?.hideGooglePlacesCredit?.(engine, LAYER_ID);
|
||||||
services.picking.unregisterPickOwner(LAYER_ID);
|
services.picking.unregisterPickOwner(LAYER_ID, engine);
|
||||||
releaseCameraSensitivity(engine, LAYER_ID);
|
releaseCameraSensitivity(engine, LAYER_ID);
|
||||||
state.surfaceChangedRemove?.();
|
state.surfaceChangedRemove?.();
|
||||||
state.surfaceChangedRemove = null;
|
state.surfaceChangedRemove = null;
|
||||||
|
|||||||
@@ -647,7 +647,7 @@ export function createLocalAdsbLayer({
|
|||||||
const restore = selectedId;
|
const restore = selectedId;
|
||||||
selectedId = null;
|
selectedId = null;
|
||||||
renderer.setVisible(true);
|
renderer.setVisible(true);
|
||||||
registerPickOwner(LAYER_ID, (id) => markers.has(id));
|
registerPickOwner(LAYER_ID, (id) => markers.has(id), renderer?.engine);
|
||||||
installInteraction();
|
installInteraction();
|
||||||
installFrame();
|
installFrame();
|
||||||
sync();
|
sync();
|
||||||
@@ -663,7 +663,7 @@ export function createLocalAdsbLayer({
|
|||||||
if (getSelectedEntityContext()?.id !== selectedId) selectedId = null;
|
if (getSelectedEntityContext()?.id !== selectedId) selectedId = null;
|
||||||
releaseInteraction();
|
releaseInteraction();
|
||||||
releaseFrame();
|
releaseFrame();
|
||||||
unregisterPickOwner(LAYER_ID);
|
unregisterPickOwner(LAYER_ID, renderer?.engine);
|
||||||
releaseTrail();
|
releaseTrail();
|
||||||
models?.destroy();
|
models?.destroy();
|
||||||
models = null;
|
models = null;
|
||||||
@@ -682,7 +682,11 @@ export function createLocalAdsbLayer({
|
|||||||
async enable() {
|
async enable() {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
renderer?.setVisible(true);
|
renderer?.setVisible(true);
|
||||||
registerPickOwner(LAYER_ID, (pickedValue) => markers.has(pickedValue));
|
registerPickOwner(
|
||||||
|
LAYER_ID,
|
||||||
|
(pickedValue) => markers.has(pickedValue),
|
||||||
|
renderer?.engine,
|
||||||
|
);
|
||||||
installInteraction();
|
installInteraction();
|
||||||
installFrame();
|
installFrame();
|
||||||
clearInterval(tickTimer);
|
clearInterval(tickTimer);
|
||||||
@@ -704,7 +708,7 @@ export function createLocalAdsbLayer({
|
|||||||
tickTimer = null;
|
tickTimer = null;
|
||||||
releaseInteraction();
|
releaseInteraction();
|
||||||
releaseFrame();
|
releaseFrame();
|
||||||
unregisterPickOwner(LAYER_ID);
|
unregisterPickOwner(LAYER_ID, renderer?.engine);
|
||||||
sync();
|
sync();
|
||||||
models?.clear();
|
models?.clear();
|
||||||
renderer?.setVisible(false);
|
renderer?.setVisible(false);
|
||||||
|
|||||||
@@ -1087,11 +1087,15 @@ test('a click on a local aircraft under an unowned or trail pick still selects i
|
|||||||
engine: harness.engine,
|
engine: harness.engine,
|
||||||
});
|
});
|
||||||
// The app's pick owners: the trails, and a Flights contact.
|
// The app's pick owners: the trails, and a Flights contact.
|
||||||
registerPickOwner('trails', (id) => String(id).startsWith('gev-trail:'));
|
registerPickOwner(
|
||||||
registerPickOwner('flights', (id) => id === 'abc123');
|
'trails',
|
||||||
|
(id) => String(id).startsWith('gev-trail:'),
|
||||||
|
harness.engine,
|
||||||
|
);
|
||||||
|
registerPickOwner('flights', (id) => id === 'abc123', harness.engine);
|
||||||
t.after(() => {
|
t.after(() => {
|
||||||
unregisterPickOwner('trails');
|
unregisterPickOwner('trails', harness.engine);
|
||||||
unregisterPickOwner('flights');
|
unregisterPickOwner('flights', harness.engine);
|
||||||
layer.destroy();
|
layer.destroy();
|
||||||
harness.release();
|
harness.release();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -119,8 +119,10 @@ export function createLifecycle({
|
|||||||
setMilitaryLayerActive(true);
|
setMilitaryLayerActive(true);
|
||||||
if (!renderer) return;
|
if (!renderer) return;
|
||||||
parts.tracking._installClickHandler();
|
parts.tracking._installClickHandler();
|
||||||
registerPickOwner('military', (pickedId) =>
|
registerPickOwner(
|
||||||
flightState._billboards.has(pickedId),
|
'military',
|
||||||
|
(pickedId) => flightState._billboards.has(pickedId),
|
||||||
|
flightState._renderer?.engine,
|
||||||
);
|
);
|
||||||
// Force a fresh rotation pass on the first tick after re-enable
|
// Force a fresh rotation pass on the first tick after re-enable
|
||||||
flightState._lastCamPoseSig = '';
|
flightState._lastCamPoseSig = '';
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export function createFirePerimetersLayer({
|
|||||||
throw new TypeError('Fire perimeters require a snapshot source');
|
throw new TypeError('Fire perimeters require a snapshot source');
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let renderer = null;
|
let renderer = null;
|
||||||
|
let sceneEngine = null;
|
||||||
const geometry = new Map();
|
const geometry = new Map();
|
||||||
let _request = null;
|
let _request = null;
|
||||||
let _linkRequest = null;
|
let _linkRequest = null;
|
||||||
@@ -230,7 +231,11 @@ export function createFirePerimetersLayer({
|
|||||||
// "empty space" — leave the selection alone and let that layer handle it.
|
// "empty space" — leave the selection alone and let that layer handle it.
|
||||||
if (picked) {
|
if (picked) {
|
||||||
const pickId = picking.resolvePickId(picked);
|
const pickId = picking.resolvePickId(picked);
|
||||||
if (pickId && picking.isOwnedByOtherLayer(layer.id, pickId)) return;
|
if (
|
||||||
|
pickId &&
|
||||||
|
picking.isOwnedByOtherLayer(layer.id, pickId, sceneEngine)
|
||||||
|
)
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (_selectedId) {
|
if (_selectedId) {
|
||||||
abortLinkVerification();
|
abortLinkVerification();
|
||||||
@@ -280,6 +285,7 @@ export function createFirePerimetersLayer({
|
|||||||
attachRenderer(viewer, { engine = null } = {}) {
|
attachRenderer(viewer, { engine = null } = {}) {
|
||||||
if (!initialized || renderer)
|
if (!initialized || renderer)
|
||||||
throw new Error('Invalid fire perimeter scene attachment');
|
throw new Error('Invalid fire perimeter scene attachment');
|
||||||
|
sceneEngine = engine;
|
||||||
renderer = createRenderer({ engine });
|
renderer = createRenderer({ engine });
|
||||||
renderer.setVisible(_enabled);
|
renderer.setVisible(_enabled);
|
||||||
if (geometry.size) renderer.replace([...geometry.values()]);
|
if (geometry.size) renderer.replace([...geometry.values()]);
|
||||||
@@ -296,6 +302,7 @@ export function createFirePerimetersLayer({
|
|||||||
overlayHost?.setVisible?.(PERIMETER_OVERLAY_SOURCE_ID, false);
|
overlayHost?.setVisible?.(PERIMETER_OVERLAY_SOURCE_ID, false);
|
||||||
renderer?.destroy();
|
renderer?.destroy();
|
||||||
renderer = null;
|
renderer = null;
|
||||||
|
sceneEngine = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
|
|||||||
@@ -54,7 +54,11 @@ export function createInteraction({
|
|||||||
const primaryStationId = stationFromPick(primaryPick);
|
const primaryStationId = stationFromPick(primaryPick);
|
||||||
if (primaryStationId) return primaryStationId;
|
if (primaryStationId) return primaryStationId;
|
||||||
const primaryId = resolvePickId(primaryPick);
|
const primaryId = resolvePickId(primaryPick);
|
||||||
if (primaryId && isOwnedByOtherLayer('radio', primaryId)) return null;
|
if (
|
||||||
|
primaryId &&
|
||||||
|
isOwnedByOtherLayer('radio', primaryId, parts.renderer?.engine)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
if (typeof renderer.drillPick === 'function') {
|
if (typeof renderer.drillPick === 'function') {
|
||||||
const drilled = renderer.drillPick(position, 16) || [];
|
const drilled = renderer.drillPick(position, 16) || [];
|
||||||
@@ -71,7 +75,11 @@ export function createInteraction({
|
|||||||
};
|
};
|
||||||
const picked = renderer.pick(offsetPosition);
|
const picked = renderer.pick(offsetPosition);
|
||||||
const pickedId = resolvePickId(picked);
|
const pickedId = resolvePickId(picked);
|
||||||
if (pickedId && isOwnedByOtherLayer('radio', pickedId)) continue;
|
if (
|
||||||
|
pickedId &&
|
||||||
|
isOwnedByOtherLayer('radio', pickedId, parts.renderer?.engine)
|
||||||
|
)
|
||||||
|
continue;
|
||||||
const stationId = stationFromPick(picked);
|
const stationId = stationFromPick(picked);
|
||||||
if (stationId) return stationId;
|
if (stationId) return stationId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ export function createRadioDisplay(engine, { state, services, parts }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderer = {
|
const renderer = {
|
||||||
|
engine,
|
||||||
get ready() {
|
get ready() {
|
||||||
return initialized;
|
return initialized;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,7 +73,11 @@ export function createInteraction({
|
|||||||
// the follow, which that sibling may have JUST started) alone (H2).
|
// the follow, which that sibling may have JUST started) alone (H2).
|
||||||
if (picked) {
|
if (picked) {
|
||||||
const pickedId = resolvePickId(picked);
|
const pickedId = resolvePickId(picked);
|
||||||
if (pickedId && isOwnedByOtherLayer('satellites', pickedId)) return;
|
if (
|
||||||
|
pickedId &&
|
||||||
|
isOwnedByOtherLayer('satellites', pickedId, layerState._engine)
|
||||||
|
)
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clicked empty space — deselect
|
// Clicked empty space — deselect
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function createLifecycle({
|
|||||||
layerState._preRenderListener =
|
layerState._preRenderListener =
|
||||||
null;
|
null;
|
||||||
document.removeEventListener('keydown', parts.interaction._onKeyDown);
|
document.removeEventListener('keydown', parts.interaction._onKeyDown);
|
||||||
unregisterPickOwner('satellites');
|
unregisterPickOwner('satellites', layerState._engine);
|
||||||
if (layerState._trackedEntity) {
|
if (layerState._trackedEntity) {
|
||||||
layerState._handoffOrigin =
|
layerState._handoffOrigin =
|
||||||
layerState._trackedEntity.gevSelectionOrigin;
|
layerState._trackedEntity.gevSelectionOrigin;
|
||||||
@@ -205,10 +205,14 @@ export function createLifecycle({
|
|||||||
if (layerState._renderer) parts.interaction._installClickHandler();
|
if (layerState._renderer) parts.interaction._installClickHandler();
|
||||||
// Pick-ownership (H2): satellite dot ids are numeric NORAD catalog numbers;
|
// Pick-ownership (H2): satellite dot ids are numeric NORAD catalog numbers;
|
||||||
// the registry hands predicates String()-coerced ids, so match via Number().
|
// the registry hands predicates String()-coerced ids, so match via Number().
|
||||||
registerPickOwner('satellites', (pickedId) => {
|
registerPickOwner(
|
||||||
const norad = Number(pickedId);
|
'satellites',
|
||||||
return Number.isFinite(norad) && layerState._points.has(norad);
|
(pickedId) => {
|
||||||
});
|
const norad = Number(pickedId);
|
||||||
|
return Number.isFinite(norad) && layerState._points.has(norad);
|
||||||
|
},
|
||||||
|
layerState._engine,
|
||||||
|
);
|
||||||
if (!layerState._preRenderListener && layerState._renderer) {
|
if (!layerState._preRenderListener && layerState._renderer) {
|
||||||
layerState._preRenderListener = getRenderer().onFrame(
|
layerState._preRenderListener = getRenderer().onFrame(
|
||||||
parts.rendering._preRenderTick,
|
parts.rendering._preRenderTick,
|
||||||
@@ -237,7 +241,7 @@ export function createLifecycle({
|
|||||||
layerState._trackedEntityChangedRemove = null;
|
layerState._trackedEntityChangedRemove = null;
|
||||||
}
|
}
|
||||||
document.removeEventListener('keydown', parts.interaction._onKeyDown);
|
document.removeEventListener('keydown', parts.interaction._onKeyDown);
|
||||||
unregisterPickOwner('satellites');
|
unregisterPickOwner('satellites', layerState._engine);
|
||||||
if (layerState._preRenderListener) {
|
if (layerState._preRenderListener) {
|
||||||
layerState._preRenderListener();
|
layerState._preRenderListener();
|
||||||
layerState._preRenderListener = null;
|
layerState._preRenderListener = null;
|
||||||
@@ -261,7 +265,7 @@ export function createLifecycle({
|
|||||||
layerState._trackedEntityChangedRemove = null;
|
layerState._trackedEntityChangedRemove = null;
|
||||||
}
|
}
|
||||||
document.removeEventListener('keydown', parts.interaction._onKeyDown);
|
document.removeEventListener('keydown', parts.interaction._onKeyDown);
|
||||||
unregisterPickOwner('satellites');
|
unregisterPickOwner('satellites', layerState._engine);
|
||||||
if (layerState._preRenderListener) {
|
if (layerState._preRenderListener) {
|
||||||
layerState._preRenderListener();
|
layerState._preRenderListener();
|
||||||
layerState._preRenderListener = null;
|
layerState._preRenderListener = null;
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ export function createSelection({ state, parts }) {
|
|||||||
if (state.clickHandler || !map?.engine) return;
|
if (state.clickHandler || !map?.engine) return;
|
||||||
state.clickHandler = map.engine.input.on('click', onClick);
|
state.clickHandler = map.engine.input.on('click', onClick);
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
picking?.registerPickOwner?.(STREET_LEVEL_LAYER_ID, parts.router.ownsPick);
|
picking?.registerPickOwner?.(
|
||||||
|
STREET_LEVEL_LAYER_ID,
|
||||||
|
parts.router.ownsPick,
|
||||||
|
state.map?.engine,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstall() {
|
function uninstall() {
|
||||||
@@ -83,7 +87,7 @@ export function createSelection({ state, parts }) {
|
|||||||
state.clickHandler();
|
state.clickHandler();
|
||||||
state.clickHandler = null;
|
state.clickHandler = null;
|
||||||
document.removeEventListener('keydown', onKeyDown);
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
picking?.unregisterPickOwner?.(STREET_LEVEL_LAYER_ID);
|
picking?.unregisterPickOwner?.(STREET_LEVEL_LAYER_ID, state.map?.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { install, uninstall, ownsPick: parts.router.ownsPick };
|
return { install, uninstall, ownsPick: parts.router.ownsPick };
|
||||||
|
|||||||
@@ -183,7 +183,11 @@ export function createLifecycle({
|
|||||||
);
|
);
|
||||||
state._overlayHost.setVisible(TRANSIT_SELECTED_OVERLAY_SOURCE_ID, true);
|
state._overlayHost.setVisible(TRANSIT_SELECTED_OVERLAY_SOURCE_ID, true);
|
||||||
parts.selection.installClickHandler(viewer);
|
parts.selection.installClickHandler(viewer);
|
||||||
registerPickOwner('transit', (pickedId) => state._vehicles.has(pickedId));
|
registerPickOwner(
|
||||||
|
'transit',
|
||||||
|
(pickedId) => state._vehicles.has(pickedId),
|
||||||
|
parts.renderer?.engine,
|
||||||
|
);
|
||||||
if (!state._cameraChangedAttached) {
|
if (!state._cameraChangedAttached) {
|
||||||
state._cameraChangedRemove = parts.renderer.onCameraChanged(
|
state._cameraChangedRemove = parts.renderer.onCameraChanged(
|
||||||
parts.viewport.onCameraChanged,
|
parts.viewport.onCameraChanged,
|
||||||
@@ -226,7 +230,7 @@ export function createLifecycle({
|
|||||||
state._visibilityTimer =
|
state._visibilityTimer =
|
||||||
null;
|
null;
|
||||||
parts.selection.removeClickHandler();
|
parts.selection.removeClickHandler();
|
||||||
unregisterPickOwner('transit');
|
unregisterPickOwner('transit', parts.renderer?.engine);
|
||||||
state._cameraChangedRemove?.();
|
state._cameraChangedRemove?.();
|
||||||
state._cameraChangedRemove = null;
|
state._cameraChangedRemove = null;
|
||||||
if (state._cameraChangedAttached) returnCameraSensitivity();
|
if (state._cameraChangedAttached) returnCameraSensitivity();
|
||||||
@@ -274,7 +278,7 @@ export function createLifecycle({
|
|||||||
parts.selection.clearSelection();
|
parts.selection.clearSelection();
|
||||||
state._overlayHost.setVisible(TRANSIT_SELECTED_OVERLAY_SOURCE_ID, false);
|
state._overlayHost.setVisible(TRANSIT_SELECTED_OVERLAY_SOURCE_ID, false);
|
||||||
parts.selection.removeClickHandler();
|
parts.selection.removeClickHandler();
|
||||||
unregisterPickOwner('transit');
|
unregisterPickOwner('transit', parts.renderer?.engine);
|
||||||
if (state._cameraChangedAttached) {
|
if (state._cameraChangedAttached) {
|
||||||
state._cameraChangedRemove?.();
|
state._cameraChangedRemove?.();
|
||||||
state._cameraChangedRemove = null;
|
state._cameraChangedRemove = null;
|
||||||
|
|||||||
@@ -640,6 +640,7 @@ export function createTransitDisplay(
|
|||||||
own(event.addEventListener((...args) => wrap(() => fn(...args))));
|
own(event.addEventListener((...args) => wrap(() => fn(...args))));
|
||||||
const scratchCartographic = geo.cartographic.create();
|
const scratchCartographic = geo.cartographic.create();
|
||||||
return {
|
return {
|
||||||
|
engine,
|
||||||
attach() {
|
attach() {
|
||||||
attached = true;
|
attached = true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ export function createLifecycle({
|
|||||||
state.preRenderRemover = null;
|
state.preRenderRemover = null;
|
||||||
components.rendering.cancelTrailingVisibilityPass();
|
components.rendering.cancelTrailingVisibilityPass();
|
||||||
releaseContinuousRender('ais-vessels');
|
releaseContinuousRender('ais-vessels');
|
||||||
unregisterPickOwner('ais-live-vessels');
|
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
|
||||||
components.tracking.detachSelectedVesselTrail();
|
components.tracking.detachSelectedVesselTrail();
|
||||||
for (const record of state.records.all)
|
for (const record of state.records.all)
|
||||||
components.rendering.removeRecordPrimitives(record);
|
components.rendering.removeRecordPrimitives(record);
|
||||||
@@ -244,7 +244,7 @@ export function createLifecycle({
|
|||||||
invalidateAisSession();
|
invalidateAisSession();
|
||||||
releaseContinuousRender('ais-vessels');
|
releaseContinuousRender('ais-vessels');
|
||||||
components.rendering.cancelTrailingVisibilityPass();
|
components.rendering.cancelTrailingVisibilityPass();
|
||||||
unregisterPickOwner('ais-live-vessels');
|
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
|
||||||
components.rendering.setVisible(false);
|
components.rendering.setVisible(false);
|
||||||
vesselState._vesselOverlayHost.clearSource(VESSEL_OVERLAY_SOURCE_ID);
|
vesselState._vesselOverlayHost.clearSource(VESSEL_OVERLAY_SOURCE_ID);
|
||||||
components.selection.clearVesselInspection();
|
components.selection.clearVesselInspection();
|
||||||
@@ -264,7 +264,7 @@ export function createLifecycle({
|
|||||||
releaseContinuousRender('ais-vessels'); // direct-destroy path (perf wave 2 fix)
|
releaseContinuousRender('ais-vessels'); // direct-destroy path (perf wave 2 fix)
|
||||||
components.rendering.cancelTrailingVisibilityPass();
|
components.rendering.cancelTrailingVisibilityPass();
|
||||||
if (state.feed.abort) state.feed.abort.abort();
|
if (state.feed.abort) state.feed.abort.abort();
|
||||||
unregisterPickOwner('ais-live-vessels');
|
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
|
||||||
components.selection.clearVesselInspection();
|
components.selection.clearVesselInspection();
|
||||||
components.tracking.destroySelectedVesselTrail();
|
components.tracking.destroySelectedVesselTrail();
|
||||||
detachFleet();
|
detachFleet();
|
||||||
@@ -289,8 +289,10 @@ export function createLifecycle({
|
|||||||
function bindVisibleScene(activeViewer, scene) {
|
function bindVisibleScene(activeViewer, scene) {
|
||||||
// Pick-ownership (H2): vessel picks carry the record OBJECT as their id;
|
// Pick-ownership (H2): vessel picks carry the record OBJECT as their id;
|
||||||
// the registry resolver reduces it to the record's mmsi (a string key).
|
// the registry resolver reduces it to the record's mmsi (a string key).
|
||||||
registerPickOwner('ais-live-vessels', (pickedId) =>
|
registerPickOwner(
|
||||||
state.records.byMmsi.has(pickedId),
|
'ais-live-vessels',
|
||||||
|
(pickedId) => state.records.byMmsi.has(pickedId),
|
||||||
|
state.renderer?.engine,
|
||||||
);
|
);
|
||||||
restoreSpriteOrderOnEnable('ais', activeViewer);
|
restoreSpriteOrderOnEnable('ais', activeViewer);
|
||||||
// Vessels are asked for by view, so ask again when the view moves away.
|
// Vessels are asked for by view, so ask again when the view moves away.
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
packImageAtlas,
|
packImageAtlas,
|
||||||
} from '../../shared/render/imageAtlas.js';
|
} from '../../shared/render/imageAtlas.js';
|
||||||
import { SPRITE_LAYER_ORDER } from '../../../data/spriteOrder.js';
|
import { SPRITE_LAYER_ORDER } from '../../../data/spriteOrder.js';
|
||||||
import { registerPickOwner } from '../../../data/pickRegistry.js';
|
|
||||||
import { allVesselChevronIcons, VESSEL_ICON_PX } from '../icons.js';
|
import { allVesselChevronIcons, VESSEL_ICON_PX } from '../icons.js';
|
||||||
|
|
||||||
const LAYER_ID = 'ais-live-vessels';
|
const LAYER_ID = 'ais-live-vessels';
|
||||||
@@ -35,13 +34,6 @@ const ATLAS_PAD = 2;
|
|||||||
/** The id namespace of track-history lines (vessels and aircraft). */
|
/** The id namespace of track-history lines (vessels and aircraft). */
|
||||||
export const TRAIL_PICK_PREFIX = 'gev-trail:';
|
export const TRAIL_PICK_PREFIX = 'gev-trail:';
|
||||||
|
|
||||||
// A trail hugs its contact, so an unclaimed trail pick would read as empty
|
|
||||||
// space in every layer's click rule and release the very contact selected.
|
|
||||||
// Claiming the namespace makes a trail pick another layer's for everyone.
|
|
||||||
registerPickOwner('trails', (pickedId) =>
|
|
||||||
String(pickedId).startsWith(TRAIL_PICK_PREFIX),
|
|
||||||
);
|
|
||||||
|
|
||||||
const iconEntry = (key, image) => ({
|
const iconEntry = (key, image) => ({
|
||||||
key,
|
key,
|
||||||
image,
|
image,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import * as Cesium from 'cesium';
|
|||||||
import * as geo from '../../../geo/index.js';
|
import * as geo from '../../../geo/index.js';
|
||||||
import { createFakeEngine } from '../../../globe/testing/fakeEngine.js';
|
import { createFakeEngine } from '../../../globe/testing/fakeEngine.js';
|
||||||
import { createCesiumMoversEngine } from '../../../testSupport/cesiumMoversEngine.mjs';
|
import { createCesiumMoversEngine } from '../../../testSupport/cesiumMoversEngine.mjs';
|
||||||
import { findPickOwner } from '../../../data/pickRegistry.js';
|
import { createPickRegistry } from '../../../data/pickRegistry.js';
|
||||||
import { SPRITE_LAYER_ORDER } from '../../../data/spriteOrder.js';
|
import { SPRITE_LAYER_ORDER } from '../../../data/spriteOrder.js';
|
||||||
import { vesselTypeCss } from '../../../data/vesselLabels.js';
|
import { vesselTypeCss } from '../../../data/vesselLabels.js';
|
||||||
import { WHITE } from '../../shared/color.js';
|
import { WHITE } from '../../shared/color.js';
|
||||||
@@ -122,6 +122,7 @@ test('the chevron atlas has one 32 px cell per chevron the layer draws', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('trail picks belong to the trail owner, so no layer reads one as empty space', () => {
|
test('trail picks belong to the trail owner, so no layer reads one as empty space', () => {
|
||||||
|
const { findPickOwner } = createPickRegistry();
|
||||||
assert.equal(findPickOwner('gev-trail:ais-live-vessels-1'), 'trails');
|
assert.equal(findPickOwner('gev-trail:ais-live-vessels-1'), 'trails');
|
||||||
assert.equal(findPickOwner('gev-trail:flights-1'), 'trails');
|
assert.equal(findPickOwner('gev-trail:flights-1'), 'trails');
|
||||||
assert.equal(findPickOwner('gev-trailing-contact'), null);
|
assert.equal(findPickOwner('gev-trailing-contact'), null);
|
||||||
|
|||||||
@@ -57,7 +57,11 @@ export function createSelection({
|
|||||||
handler.setInputAction((click) =>
|
handler.setInputAction((click) =>
|
||||||
handleClick({
|
handleClick({
|
||||||
position: click.position,
|
position: click.position,
|
||||||
picked: pickContact(viewer.scene, click.position),
|
picked: pickContact(
|
||||||
|
viewer.scene,
|
||||||
|
click.position,
|
||||||
|
state.renderer?.engine,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// The follow signal of the layer's renderer.
|
// The follow signal of the layer's renderer.
|
||||||
@@ -98,12 +102,18 @@ export function createSelection({
|
|||||||
// A sibling layer already owns this click. Preserve the current vessel
|
// A sibling layer already owns this click. Preserve the current vessel
|
||||||
// selection and do not compete with its camera command.
|
// selection and do not compete with its camera command.
|
||||||
const siblingOwned =
|
const siblingOwned =
|
||||||
pickedId && isOwnedByOtherLayer('ais-live-vessels', pickedId);
|
pickedId &&
|
||||||
|
isOwnedByOtherLayer('ais-live-vessels', pickedId, state.renderer?.engine);
|
||||||
// Cyclone geometry is ambient context behind painted cards. Its handler
|
// Cyclone geometry is ambient context behind painted cards. Its handler
|
||||||
// yields to the same foreground AIS hit. Keep moving-contact precedence
|
// yields to the same foreground AIS hit. Keep moving-contact precedence
|
||||||
// unchanged: those handlers may issue their own camera command.
|
// unchanged: those handlers may issue their own camera command.
|
||||||
const cycloneBackground =
|
const cycloneBackground =
|
||||||
siblingOwned && !isOwnedByOtherLayer('weather-cyclones', pickedId);
|
siblingOwned &&
|
||||||
|
!isOwnedByOtherLayer(
|
||||||
|
'weather-cyclones',
|
||||||
|
pickedId,
|
||||||
|
state.renderer?.engine,
|
||||||
|
);
|
||||||
if (siblingOwned && !cycloneBackground) return;
|
if (siblingOwned && !cycloneBackground) return;
|
||||||
|
|
||||||
// Cards are painted on a pointer-events:none canvas, so the scene pick is
|
// Cards are painted on a pointer-events:none canvas, so the scene pick is
|
||||||
|
|||||||
@@ -527,6 +527,7 @@ export function createLifecycle({ state, services, parts }) {
|
|||||||
typeof id === 'string' &&
|
typeof id === 'string' &&
|
||||||
id.startsWith('wildlife:') &&
|
id.startsWith('wildlife:') &&
|
||||||
state.rowById.has(id.slice(9)),
|
state.rowById.has(id.slice(9)),
|
||||||
|
state.engine,
|
||||||
);
|
);
|
||||||
state.removeCamera =
|
state.removeCamera =
|
||||||
state.engine.camera.moveEnd.addEventListener(onMoveEnd);
|
state.engine.camera.moveEnd.addEventListener(onMoveEnd);
|
||||||
@@ -537,7 +538,7 @@ export function createLifecycle({ state, services, parts }) {
|
|||||||
state.removeCamera = null;
|
state.removeCamera = null;
|
||||||
parts.rendering.deactivate?.();
|
parts.rendering.deactivate?.();
|
||||||
parts.selection.uninstall?.();
|
parts.selection.uninstall?.();
|
||||||
services.picking?.unregisterPickOwner?.('wildlife-sightings');
|
services.picking?.unregisterPickOwner?.('wildlife-sightings', state.engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Requests, timers and debounces in flight end with their scene or lifetime.
|
// Requests, timers and debounces in flight end with their scene or lifetime.
|
||||||
|
|||||||
@@ -504,7 +504,14 @@ export function createSelection({ state, services, parts }) {
|
|||||||
selectAndFocus(id.slice(9))
|
selectAndFocus(id.slice(9))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
if (id && services.picking?.isOwnedByOtherLayer?.('wildlife-sightings', id))
|
if (
|
||||||
|
id &&
|
||||||
|
services.picking?.isOwnedByOtherLayer?.(
|
||||||
|
'wildlife-sightings',
|
||||||
|
id,
|
||||||
|
state.engine,
|
||||||
|
)
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
clearAndRebuild();
|
clearAndRebuild();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function createPointerPicker({
|
|||||||
} catch {
|
} catch {
|
||||||
picked = null;
|
picked = null;
|
||||||
}
|
}
|
||||||
const entity = resolvePickedContext(picked, { dataManager });
|
const entity = resolvePickedContext(picked, { dataManager, engine });
|
||||||
let ground = null;
|
let ground = null;
|
||||||
try {
|
try {
|
||||||
const hit = pickWorldFromScreen(engine, x / width, y / height);
|
const hit = pickWorldFromScreen(engine, x / width, y / height);
|
||||||
|
|||||||
Reference in new issue
Block a user