mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-09 21:02:19 +00:00
Follow, credits, embed views and FPS through the engine on Navara
Five places still read the Cesium viewer and so did nothing, or the wrong thing, on Navara. Each now goes through the globe contract, and Cesium behaves as before. - The Navara ground floor stands aside while the camera inputs are suspended, not only while they are switched off. The cockpit suspends them, so its eye 12 m up no longer jumps to the floor's 30 m between updates. A follow's own suspension keeps the floor on, as before. - Follow state comes from engine.camera.following() and followChanged instead of viewer.trackedEntity. Enabling CCTV during a follow no longer flies away and ends it, a new follow stamps navigation, Director cards wait while a subject is followed, and an explicit navigation stops the follow on both engines. - Navara registers every data credit, as Cesium does. The allowlist left out the layers that moved to Navara later (CCTV agencies, street-level imagery, GIBS, routing, bike feeds and others). - Annotation links and embed mode attach on every engine; their camera flights use engine.camera.flyTo. Only Cesium adds the timer that keeps an inline panel drawing. - The FPS readout counts engine.frames.postRender, so it shows on Navara. Each fix has a test that fails without it.
This commit is contained in:
1 parent
e1095ee553
commit
18f69c2699
23 files changed
+144
-158
No files matched your search
+15
-11
@@ -144,10 +144,10 @@ const delay = (ms, signal) =>
|
||||
});
|
||||
|
||||
/** Fly the camera to a view's camera and resolve when the flight ends. */
|
||||
function flyTo(viewer, camera) {
|
||||
function flyTo(engine, camera) {
|
||||
announceNavigationAuthority('embed-view');
|
||||
return new Promise((resolve) => {
|
||||
viewer.camera.flyTo({
|
||||
engine.camera.flyTo({
|
||||
destination: geo.cartesian3.fromDegrees(
|
||||
camera.lon,
|
||||
camera.lat,
|
||||
@@ -173,7 +173,7 @@ function flyTo(viewer, camera) {
|
||||
*/
|
||||
export async function applyView(
|
||||
view,
|
||||
{ viewer, dataManager, run, signal, retryMs = FOLLOW_RETRY_MS },
|
||||
{ engine, dataManager, run, signal, retryMs = FOLLOW_RETRY_MS },
|
||||
) {
|
||||
const steps = [];
|
||||
const act = async (name, args) => {
|
||||
@@ -208,7 +208,7 @@ export async function applyView(
|
||||
if (view.annotations.length)
|
||||
await act('annotate_map', { annotations: [...view.annotations] });
|
||||
if (!view.follow) {
|
||||
steps.push({ step: 'camera', ok: await flyTo(viewer, view.camera) });
|
||||
steps.push({ step: 'camera', ok: await flyTo(engine, view.camera) });
|
||||
return steps;
|
||||
}
|
||||
// A followed entity owns the camera, and a camera flight would end the
|
||||
@@ -227,7 +227,7 @@ export async function applyView(
|
||||
};
|
||||
let followed = await follow();
|
||||
if (!followed) {
|
||||
steps.push({ step: 'camera', ok: await flyTo(viewer, view.camera) });
|
||||
steps.push({ step: 'camera', ok: await flyTo(engine, view.camera) });
|
||||
for (let attempt = 1; attempt < FOLLOW_ATTEMPTS && !followed; attempt++) {
|
||||
if (signal?.aborted) break;
|
||||
await delay(retryMs, signal);
|
||||
@@ -242,11 +242,14 @@ export async function applyView(
|
||||
|
||||
/**
|
||||
* Install embed mode when the page asks for it, and draw any annotations the
|
||||
* opening link carries. Returns a function that removes the message handler.
|
||||
* opening link carries. `keepRendering({ windowRef })`, when the engine needs
|
||||
* it, keeps an inline panel drawing (keepPanelRendering). Returns a function
|
||||
* that removes the message handler.
|
||||
*/
|
||||
export function installViews({
|
||||
shell,
|
||||
viewer,
|
||||
engine,
|
||||
keepRendering = null,
|
||||
dataManager,
|
||||
run,
|
||||
signal,
|
||||
@@ -265,9 +268,10 @@ export function installViews({
|
||||
|
||||
windowRef.document.body.classList.add('ui-embed');
|
||||
shell.setCleanView?.(true);
|
||||
const stopRendering = isEmbeddedInline()
|
||||
? keepPanelRendering(viewer, { windowRef })
|
||||
: () => {};
|
||||
const stopRendering =
|
||||
isEmbeddedInline() && keepRendering
|
||||
? keepRendering({ windowRef })
|
||||
: () => {};
|
||||
// A framing page talks to the app across frames; an inline panel shares
|
||||
// the page with it and talks through the page's own window.
|
||||
const peer = isEmbeddedInline() ? windowRef : windowRef.parent;
|
||||
@@ -304,7 +308,7 @@ export function installViews({
|
||||
}
|
||||
queue = queue.then(async () => {
|
||||
if (signal?.aborted) return;
|
||||
const steps = await applyView(view, { viewer, dataManager, run, signal });
|
||||
const steps = await applyView(view, { engine, dataManager, run, signal });
|
||||
post(
|
||||
{
|
||||
type: EMBED_APPLIED_MESSAGE,
|
||||
|
||||
+11
-11
@@ -10,7 +10,7 @@ import {
|
||||
import { LayerLifecycle } from '../data/lifecycle.js';
|
||||
import { createView } from '../view/index.js';
|
||||
|
||||
const fakeViewer = () => {
|
||||
const fakeEngine = () => {
|
||||
const flights = [];
|
||||
return {
|
||||
flights,
|
||||
@@ -48,7 +48,7 @@ test('embed mode is opt-in by query parameter', () => {
|
||||
});
|
||||
|
||||
test('a view is applied through the app actions in order', async () => {
|
||||
const viewer = fakeViewer();
|
||||
const engine = fakeEngine();
|
||||
const { calls, run } = recorder();
|
||||
const view = createView({
|
||||
camera: { lat: 25, lon: 121, altitude_m: 300000, pitch_deg: -40 },
|
||||
@@ -58,7 +58,7 @@ test('a view is applied through the app actions in order', async () => {
|
||||
annotations: [{ type: 'pin', target: 'Taipei 101' }],
|
||||
});
|
||||
const steps = await applyView(view, {
|
||||
viewer,
|
||||
engine,
|
||||
dataManager: fakeLayers(new Set(['earthquakes'])),
|
||||
run,
|
||||
});
|
||||
@@ -78,7 +78,7 @@ test('a view is applied through the app actions in order', async () => {
|
||||
['annotate_map', null, null],
|
||||
],
|
||||
);
|
||||
assert.equal(viewer.flights.length, 1);
|
||||
assert.equal(engine.flights.length, 1);
|
||||
assert.ok(steps.every((step) => step.ok));
|
||||
assert.ok(steps.some((step) => step.step === 'camera'));
|
||||
});
|
||||
@@ -93,7 +93,7 @@ test('a followed entity is retried until its layer has it', async () => {
|
||||
follow: { kind: 'aircraft', id: 'abc123' },
|
||||
});
|
||||
const steps = await applyView(view, {
|
||||
viewer: fakeViewer(),
|
||||
engine: fakeEngine(),
|
||||
dataManager: fakeLayers(new Set()),
|
||||
run,
|
||||
retryMs: 1,
|
||||
@@ -125,7 +125,7 @@ test('an embedded page takes views only from its parent and answers it', async (
|
||||
const { calls, run } = recorder();
|
||||
const remove = installViews({
|
||||
shell,
|
||||
viewer: fakeViewer(),
|
||||
engine: fakeEngine(),
|
||||
dataManager: fakeLayers(new Set()),
|
||||
run,
|
||||
location: { search: '?embed=1', hash: '' },
|
||||
@@ -204,7 +204,7 @@ test('an app loaded inline into a panel page talks through its own window', asyn
|
||||
assert.equal(isEmbedded({ search: '' }), true);
|
||||
const remove = installViews({
|
||||
shell: { initialRestorePromise: Promise.resolve() },
|
||||
viewer: fakeViewer(),
|
||||
engine: fakeEngine(),
|
||||
dataManager: fakeLayers(new Set()),
|
||||
run: recorder().run,
|
||||
location: { search: '', hash: '' },
|
||||
@@ -288,7 +288,7 @@ test('annotations in a link are drawn after it restores, embedded or not', async
|
||||
);
|
||||
installViews({
|
||||
shell: { initialRestorePromise: Promise.resolve() },
|
||||
viewer: fakeViewer(),
|
||||
engine: fakeEngine(),
|
||||
dataManager: fakeLayers(new Set()),
|
||||
run,
|
||||
location: { search: '', hash: `#v=2&lat=1&lon=2&an=${an}` },
|
||||
@@ -368,17 +368,17 @@ test('a panel reports a render error in full and stops drawing', () => {
|
||||
});
|
||||
|
||||
test('a followed aircraft owns the camera, and cockpit view is entered after it', async () => {
|
||||
const viewer = fakeViewer();
|
||||
const engine = fakeEngine();
|
||||
const { calls, run } = recorder();
|
||||
const steps = await applyView(
|
||||
createView({
|
||||
camera: { lat: 32.7, lon: -117.2 },
|
||||
follow: { kind: 'military_aircraft', id: 'AE1234', cockpit: true },
|
||||
}),
|
||||
{ viewer, dataManager: fakeLayers(new Set()), run, retryMs: 0 },
|
||||
{ engine, dataManager: fakeLayers(new Set()), run, retryMs: 0 },
|
||||
);
|
||||
// Following worked at once, so no camera flight ended it.
|
||||
assert.equal(viewer.flights.length, 0);
|
||||
assert.equal(engine.flights.length, 0);
|
||||
assert.deepEqual(calls.slice(-2), [
|
||||
['track_entity', { query: 'ae1234', layerId: 'military' }],
|
||||
['control_cockpit', { action: 'enter', targetLayer: 'military' }],
|
||||
|
||||
+2
-34
@@ -5,7 +5,7 @@ import { t } from '../i18n/index.js';
|
||||
// loads the Navara globe the same way, so the Cesium-mode bundle carries
|
||||
// none of it.
|
||||
|
||||
import { DATA_CREDITS, registerDataCredits } from '../data/dataCredits.js';
|
||||
import { registerDataCredits } from '../data/dataCredits.js';
|
||||
import { createLayerAvailability } from './layers/renderers.js';
|
||||
import { createNavaraAnnotationRenderer } from '../annotations/navaraAnnotationRenderer.js';
|
||||
|
||||
@@ -29,35 +29,6 @@ export function navaraModeNotice(availability) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Static attribution for the shell and the layers available on Navara. */
|
||||
const NAVARA_CREDIT_KEYS = new Set([
|
||||
'inaturalist',
|
||||
'photon-geocoder',
|
||||
'openstreetmap',
|
||||
'reearth-terrain',
|
||||
'opensky',
|
||||
'adsblol',
|
||||
'adsbdb',
|
||||
'aisstream',
|
||||
'celestrak',
|
||||
'launch-library-2',
|
||||
'usgs',
|
||||
'eccc-aqhi',
|
||||
'wfigs',
|
||||
's2-ghostmaps',
|
||||
'telegeography',
|
||||
'wind-gfs',
|
||||
'ecmwf-ifs',
|
||||
'weather-noaa',
|
||||
'weather-lightning',
|
||||
'weather-cyclones',
|
||||
'dams',
|
||||
'firms',
|
||||
'gbfs',
|
||||
'overture-military-names',
|
||||
'radio-browser',
|
||||
]);
|
||||
|
||||
/**
|
||||
* The scene on the experimental Navara engine: no viewer and no Cesium
|
||||
* objects. Navara builds the map sources from the controller's descriptions
|
||||
@@ -109,10 +80,7 @@ export async function createNavaraScene({
|
||||
signal.throwIfAborted();
|
||||
// After the map's own credits, so Google's copyright lines come first in
|
||||
// Navara's attribution card.
|
||||
registerDataCredits(
|
||||
engine,
|
||||
(credits ?? DATA_CREDITS).filter(({ key }) => NAVARA_CREDIT_KEYS.has(key)),
|
||||
);
|
||||
registerDataCredits(engine, credits);
|
||||
return {
|
||||
viewer: null,
|
||||
engine,
|
||||
|
||||
@@ -174,30 +174,11 @@ test('the Navara scene tears down the controller, then the globe', async (t) =>
|
||||
assert.equal(view.__GOOGLE_MAPS_API_KEY__, undefined);
|
||||
});
|
||||
|
||||
test('Navara keeps attribution for every available W1/W2 source and excludes unavailable data', async (t) => {
|
||||
const keys = [
|
||||
'opensky',
|
||||
'adsblol',
|
||||
'adsbdb',
|
||||
'aisstream',
|
||||
'celestrak',
|
||||
'launch-library-2',
|
||||
'usgs',
|
||||
'eccc-aqhi',
|
||||
'wfigs',
|
||||
'firms',
|
||||
'gbfs',
|
||||
'overture-military-names',
|
||||
'radio-browser',
|
||||
'photon-geocoder',
|
||||
'openstreetmap',
|
||||
'reearth-terrain',
|
||||
];
|
||||
test('Navara registers the attribution of every data source, as Cesium does', async (t) => {
|
||||
// Every layer draws on Navara, so none of their credits may be dropped.
|
||||
const keys = ['opensky', 'nasa-gibs', 'nyc-dot-cctv', 'mapillary', 'gdelt'];
|
||||
const run = await startScene({
|
||||
credits: [...keys, 'nasa-gibs'].map((key) => ({
|
||||
key,
|
||||
html: `Credit ${key}`,
|
||||
})),
|
||||
credits: keys.map((key) => ({ key, html: `Credit ${key}` })),
|
||||
});
|
||||
t.after(run.teardown);
|
||||
assert.deepEqual(
|
||||
|
||||
+14
-19
@@ -13,7 +13,7 @@ import {
|
||||
currentSceneOf,
|
||||
detachPartsOnTeardown,
|
||||
} from './sceneParts.js';
|
||||
import { installViews, isEmbeddedInline } from './embed.js';
|
||||
import { installViews, isEmbeddedInline, keepPanelRendering } from './embed.js';
|
||||
import {
|
||||
installScopeMask,
|
||||
destroyScopeMask,
|
||||
@@ -310,20 +310,8 @@ function cesiumToolBackend({ engine, tileset }) {
|
||||
viewer.useDefaultRenderLoop = !hidden;
|
||||
cockpitCloudEffects?.setSuspended?.(hidden);
|
||||
},
|
||||
attachViews(
|
||||
{ styleManager, dataManager, voiceCommands },
|
||||
{ signal, defer },
|
||||
) {
|
||||
defer(
|
||||
installViews({
|
||||
shell: styleManager,
|
||||
viewer,
|
||||
dataManager,
|
||||
run: (name, args) => voiceCommands.runner(name, args, { signal }),
|
||||
signal,
|
||||
}),
|
||||
);
|
||||
},
|
||||
// An inline panel the host reports hidden stops Cesium's render loop.
|
||||
keepRendering: (options) => keepPanelRendering(viewer, options),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -336,7 +324,7 @@ function cesiumToolBackend({ engine, tileset }) {
|
||||
* this engine), `recentImagery(layer)` the panel's globe,
|
||||
* `attach(context, attachment)` adds
|
||||
* the engine's own tools, `suspend(hidden, controls)` its hidden-tab
|
||||
* handling and `attachViews(context, attachment)` its embedded views. The
|
||||
* handling and `keepRendering` what keeps an inline panel drawing. The
|
||||
* shared tools keep their session state across engines. Returns the
|
||||
* annotation board (null without one) and voice commands.
|
||||
*/
|
||||
@@ -460,9 +448,16 @@ async function attachEngineTools(
|
||||
signal,
|
||||
defer,
|
||||
});
|
||||
backend.attachViews?.(
|
||||
{ styleManager, dataManager, voiceCommands },
|
||||
{ signal, defer },
|
||||
// Annotation links and embed mode, on every engine.
|
||||
defer(
|
||||
installViews({
|
||||
shell: styleManager,
|
||||
engine,
|
||||
keepRendering: backend.keepRendering ?? null,
|
||||
dataManager,
|
||||
run: (name, args) => voiceCommands.runner(name, args, { signal }),
|
||||
signal,
|
||||
}),
|
||||
);
|
||||
return { annotations, voiceCommands, startupChrome: backend.startupChrome };
|
||||
}
|
||||
|
||||
@@ -17,9 +17,17 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
||||
window: globalThis.window,
|
||||
document: globalThis.document,
|
||||
localStorage: globalThis.localStorage,
|
||||
location: globalThis.location,
|
||||
};
|
||||
const listeners = new Map();
|
||||
globalThis.window = {};
|
||||
// A link that carries an annotation, drawn once the view restores.
|
||||
const linked = encodeURIComponent(
|
||||
JSON.stringify([{ type: 'pin', target: 'Austin' }]),
|
||||
);
|
||||
globalThis.location = { search: '', hash: `#v=2&lat=1&lon=2&an=${linked}` };
|
||||
let restored;
|
||||
const initialRestorePromise = new Promise((resolve) => (restored = resolve));
|
||||
globalThis.localStorage = {
|
||||
getItem: () => null,
|
||||
setItem() {},
|
||||
@@ -54,11 +62,14 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
||||
// The voice pointer listens on the engine's canvas.
|
||||
const canvas = new EventTarget();
|
||||
const engine = createFakeEngine({ kind: 'navara', canvas });
|
||||
// Navara's extension without its view: no annotation board.
|
||||
engine.extensions = { navara: { alignDaylightToRestoredView() {} } };
|
||||
const notices = [];
|
||||
const chrome = [];
|
||||
const cleanups = [];
|
||||
let flushed = 0;
|
||||
const styleManager = {
|
||||
initialRestorePromise,
|
||||
_showGlobalStatusNotice: (label, options) =>
|
||||
notices.push({ label, options }),
|
||||
};
|
||||
@@ -102,6 +113,18 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
||||
defer: (dispose) => cleanups.push(dispose),
|
||||
});
|
||||
assert.ok(tools.sceneDirector);
|
||||
const ran = [];
|
||||
tools.voiceCommands.runner = async (name, args) => {
|
||||
ran.push([name, args]);
|
||||
return { ok: true };
|
||||
};
|
||||
restored();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.deepEqual(
|
||||
ran,
|
||||
[['annotate_map', { annotations: [{ type: 'pin', target: 'Austin' }] }]],
|
||||
"the link's annotations are drawn on this engine too",
|
||||
);
|
||||
assert.equal(tools.annotations, null);
|
||||
assert.ok(tools.voiceCommands, 'voice runs on the engine');
|
||||
assert.equal(window.__gevVoiceCommands, tools.voiceCommands);
|
||||
|
||||
@@ -137,8 +137,8 @@ test('voice Cockpit entry reaches the camera only through stamping seams', () =>
|
||||
//
|
||||
// The transaction has exactly two camera-owner mutations, and each one
|
||||
// stamps:
|
||||
// 1. selectedLayer.trackById(id) -> viewer.trackedEntity
|
||||
// -> viewer.trackedEntityChanged -> _stampNavigation()
|
||||
// 1. selectedLayer.trackById(id) -> engine.camera.follow
|
||||
// -> engine.camera.followChanged -> _stampNavigation()
|
||||
// 2. cockpitView.enter() -> onCameraTakeover() -> _stampNavigation()
|
||||
const transaction = body(
|
||||
cockpitTracking,
|
||||
@@ -162,7 +162,7 @@ test('voice Cockpit entry reaches the camera only through stamping seams', () =>
|
||||
// Seam 1: a new tracker handoff stamps; restoring the session does not.
|
||||
assert.match(
|
||||
ui,
|
||||
/viewer\.trackedEntityChanged\.addEventListener\(\s*\(entity\) => \{[\s\S]*?if \(entity && !this\._disposed && !entity\.gevRestoring\)\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\);/,
|
||||
/camera\.followChanged\.addEventListener\(\(handle\) => \{[\s\S]*?if \(handle && !this\._disposed && !handle\.native\?\.gevRestoring\)\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\);/,
|
||||
'tracker handoff must stamp',
|
||||
);
|
||||
// Seam 2 is pinned by "Cockpit takeover invalidates deferred work" above.
|
||||
@@ -255,7 +255,7 @@ test('accepted navigation releases through PR15-aware ownership before flight',
|
||||
'origin: trackingOrigin',
|
||||
'satellitesLayer.stopTracking?.({ origin: trackingOrigin })',
|
||||
'rocketLaunchesLayer.releaseCameraOwnership?.()',
|
||||
'if (this.viewer) this.viewer.trackedEntity = undefined;',
|
||||
'this.engine?.camera.following()?.stop();',
|
||||
"interruptCameraMotion('explicit-navigation')",
|
||||
'camera?.cancelFlight();',
|
||||
'camera?.release();',
|
||||
|
||||
@@ -259,12 +259,15 @@ export function createNavaraEngine(
|
||||
}) ?? null;
|
||||
const { surface } = createNavaraSurface(view, hooks);
|
||||
// Navara's controller zooms through the ground; keep the camera above it.
|
||||
// With the camera inputs off nothing zooms, and the app places the camera
|
||||
// itself (the cockpit eye, 12 m up): the floor's 30 m would fight it.
|
||||
// With the camera inputs off or suspended nothing zooms, and the app places
|
||||
// the camera itself (the cockpit eye, 12 m up): the floor's 30 m would
|
||||
// fight it. A follow suspends them only to take drag and wheel itself.
|
||||
groundFloor = createGroundFloor({
|
||||
surface,
|
||||
position: () => model.camera.livePosition,
|
||||
isFree: () => model.isFree() && input.cameraInputs(),
|
||||
isFree: () =>
|
||||
model.isFree() &&
|
||||
(input.input.cameraInputsEnabled() || Boolean(follow.following())),
|
||||
lift: (position) => model.lift(position),
|
||||
setMinimumZoomDistance: (distance) =>
|
||||
input.input.setCameraOptions({ minimumZoomDistance: distance }),
|
||||
|
||||
@@ -577,6 +577,21 @@ test('the floor stands aside while the camera inputs are off (the cockpit drives
|
||||
engine.destroy();
|
||||
});
|
||||
|
||||
test('the floor stands aside while the camera inputs are suspended (the cockpit holds them)', () => {
|
||||
const { engine, view } = setup();
|
||||
view.sampleTerrainHeight = () => 100;
|
||||
const resume = engine.input.suspendCameraInputs();
|
||||
engine.camera.setView({
|
||||
destination: cartesian3.fromDegrees(2.35, 48.85, 112),
|
||||
});
|
||||
view.emit('preRender', 1);
|
||||
near(heightOf(engine), 112, 1e-6, 'the cockpit eye 12 m up is kept');
|
||||
resume();
|
||||
view.emit('preRender', 2);
|
||||
near(heightOf(engine), 130, 1e-6, 'lifted once the inputs are back');
|
||||
engine.destroy();
|
||||
});
|
||||
|
||||
test('the ground floor reports its own ground and margin (diagnostics, not contract)', () => {
|
||||
const { engine } = setup();
|
||||
assert.equal(typeof engine.groundFloor.ground, 'function');
|
||||
|
||||
@@ -11,13 +11,13 @@ function observedShell(t) {
|
||||
t.after(() => {
|
||||
globalThis.window = previous;
|
||||
});
|
||||
const trackedEntityChanged = createEngineEvent();
|
||||
const followChanged = createEngineEvent();
|
||||
const shell = {
|
||||
generation: 1,
|
||||
trackedEntityChanged,
|
||||
followChanged,
|
||||
};
|
||||
const bindings = new LayerBindings({
|
||||
viewer: { trackedEntityChanged },
|
||||
engine: { camera: { followChanged } },
|
||||
services: {},
|
||||
operations: {
|
||||
_stampNavigation() {
|
||||
@@ -32,10 +32,10 @@ function observedShell(t) {
|
||||
|
||||
// What the tracking modules do around follow.start on a resume or a share
|
||||
// restore (the shared aircraft tracking.js and satellites tracking.js).
|
||||
function follow(trackedEntityChanged, entity, restoring) {
|
||||
function follow(followChanged, entity, restoring) {
|
||||
if (restoring) entity.gevRestoring = true;
|
||||
try {
|
||||
trackedEntityChanged.raiseEvent(entity);
|
||||
followChanged.raiseEvent({ native: entity });
|
||||
} finally {
|
||||
if (restoring) delete entity.gevRestoring;
|
||||
}
|
||||
@@ -86,7 +86,7 @@ test('resuming an aircraft on Cesium preserves the pending share navigation', (t
|
||||
(id, options) => {
|
||||
restored = { id, ...options };
|
||||
follow(
|
||||
shell.trackedEntityChanged,
|
||||
shell.followChanged,
|
||||
{ gevSelectionOrigin: options.origin },
|
||||
options.resume,
|
||||
);
|
||||
@@ -100,7 +100,7 @@ test('resuming an aircraft on Cesium preserves the pending share navigation', (t
|
||||
'restored tracking cannot supersede and clear itself',
|
||||
);
|
||||
for (const origin of ['user', 'voice', 'programmatic']) {
|
||||
follow(shell.trackedEntityChanged, { gevSelectionOrigin: origin }, false);
|
||||
follow(shell.followChanged, { gevSelectionOrigin: origin }, false);
|
||||
}
|
||||
assert.equal(
|
||||
shell.generation,
|
||||
@@ -112,12 +112,12 @@ test('resuming an aircraft on Cesium preserves the pending share navigation', (t
|
||||
test('a restored aircraft followed again later is new navigation', (t) => {
|
||||
const shell = observedShell(t);
|
||||
const entity = { gevSelectionOrigin: 'share-restore' };
|
||||
follow(shell.trackedEntityChanged, entity, true);
|
||||
follow(shell.followChanged, entity, true);
|
||||
assert.equal(shell.generation, 1);
|
||||
assert.equal('gevRestoring' in entity, false, 'the mark does not stick');
|
||||
// Cockpit exit or a refocus hands the same aircraft back to the camera.
|
||||
shell.trackedEntityChanged.raiseEvent(undefined);
|
||||
shell.trackedEntityChanged.raiseEvent(entity);
|
||||
shell.followChanged.raiseEvent(null);
|
||||
shell.followChanged.raiseEvent({ native: entity });
|
||||
assert.equal(shell.generation, 2);
|
||||
});
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ export class SceneDirector {
|
||||
available: () =>
|
||||
!this._destroyed &&
|
||||
!this._running &&
|
||||
!this._scene?.viewer?.trackedEntity,
|
||||
!this._scene?.engine?.camera.following(),
|
||||
execute: (action, signal) =>
|
||||
this._trackWork(this._executeInteraction(action, signal)),
|
||||
});
|
||||
|
||||
@@ -2121,8 +2121,7 @@ for (const createHarness of GROUND_HARNESSES) {
|
||||
const follow = engine.camera.follow({ position: () => ({ x: 6378137, y: 0, z: 1000 }), viewFrom: { x: 0, y: -4800, z: 3300 } });
|
||||
store.controls.dispatch('card', store.panel.owner);
|
||||
await settle();
|
||||
if (harness.cesium) assert.equal(store.panel.card, null, 'tracked subjects keep camera ownership');
|
||||
else assert.equal(store.panel.card.text, 'Details', 'the engine without a legacy tracked subject keeps its interaction policy');
|
||||
assert.equal(store.panel.card, null, 'a followed subject keeps camera ownership');
|
||||
follow.stop();
|
||||
store.controls.dispatch('card', store.panel.owner);
|
||||
await settle();
|
||||
|
||||
@@ -314,7 +314,7 @@ export class StyleManager extends ShellScene {
|
||||
|
||||
this._initSceneOverlays();
|
||||
this._displayBindings = new DisplayBindings({
|
||||
viewer,
|
||||
engine,
|
||||
services: {
|
||||
cycleDetectionMode: services.cycleDetectionMode,
|
||||
isScopeMaskEnabled: services.isScopeMaskEnabled,
|
||||
@@ -1001,7 +1001,7 @@ export class StyleManager extends ShellScene {
|
||||
setEnabled: (next) =>
|
||||
this._dataManager.setEnabled('cctv', next, { origin: 'user' }),
|
||||
readOwnership: () => ({
|
||||
trackedEntity: this.viewer?.trackedEntity,
|
||||
trackedEntity: this.engine?.camera.following() ?? null,
|
||||
cockpitActive: !!this.cockpitView?.active,
|
||||
}),
|
||||
shouldFocus: () =>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { applicationPinia } from './vueRuntime.js';
|
||||
/** Own keyboard/display event subscriptions; settings remain with their state owners. */
|
||||
export class DisplayBindings {
|
||||
constructor({
|
||||
viewer,
|
||||
engine,
|
||||
services,
|
||||
elements,
|
||||
operations,
|
||||
@@ -16,7 +16,7 @@ export class DisplayBindings {
|
||||
}) {
|
||||
Object.assign(
|
||||
this,
|
||||
{ viewer, services, readState, claimDetection, displayStore },
|
||||
{ engine, services, readState, claimDetection, displayStore },
|
||||
elements,
|
||||
operations,
|
||||
);
|
||||
@@ -45,7 +45,7 @@ export class DisplayBindings {
|
||||
_initUI() {
|
||||
const { cycleDetectionMode, isScopeMaskEnabled } = this.services;
|
||||
this._applicationShortcuts?.destroy();
|
||||
this.attachViewer(this.viewer);
|
||||
this.attachEngine(this.engine);
|
||||
this._applicationShortcuts = bindApplicationShortcuts({
|
||||
documentRef: document,
|
||||
searchInput: this._locationSearch,
|
||||
@@ -168,14 +168,14 @@ export class DisplayBindings {
|
||||
this._detachDisplayControls = () => store.detach(token);
|
||||
}
|
||||
/**
|
||||
* Count the frames of another viewer (an engine switch), or none. The
|
||||
* Count the frames of another engine (an engine switch), or none. The
|
||||
* shortcuts and the Display controls stay bound.
|
||||
*/
|
||||
attachViewer(viewer) {
|
||||
this.viewer = viewer ?? null;
|
||||
attachEngine(engine) {
|
||||
this.engine = engine ?? null;
|
||||
this._frameRateMonitor?.destroy();
|
||||
this._frameRateMonitor = createFrameRateMonitor({
|
||||
viewer: this.viewer,
|
||||
engine: this.engine,
|
||||
documentRef: document,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { applicationPinia } from './vueRuntime.js';
|
||||
* readout is presented to the status store, which the title bar renders.
|
||||
*/
|
||||
export function createFrameRateMonitor({
|
||||
viewer,
|
||||
engine,
|
||||
documentRef = document,
|
||||
store = useStatusStore(applicationPinia()),
|
||||
}) {
|
||||
const frameEvent = viewer?.scene?.postRender;
|
||||
const frameEvent = engine?.frames.postRender;
|
||||
// A page without the scene chrome has no title bar to show it in.
|
||||
if (!documentRef.getElementById('title-bar') || !frameEvent)
|
||||
return { destroy() {} };
|
||||
|
||||
@@ -17,8 +17,8 @@ function fixture(t) {
|
||||
addEventListener: (name, handler) => keys.add(handler),
|
||||
removeEventListener: (name, handler) => keys.delete(handler),
|
||||
};
|
||||
const viewer = {
|
||||
scene: {
|
||||
const engine = {
|
||||
frames: {
|
||||
postRender: {
|
||||
addEventListener(handler) {
|
||||
frames.add(handler);
|
||||
@@ -27,7 +27,7 @@ function fixture(t) {
|
||||
},
|
||||
},
|
||||
};
|
||||
const monitor = createFrameRateMonitor({ viewer, documentRef, store });
|
||||
const monitor = createFrameRateMonitor({ engine, documentRef, store });
|
||||
t.after(() => monitor.destroy());
|
||||
return {
|
||||
monitor,
|
||||
@@ -108,7 +108,7 @@ test('a page without the title bar gets no readout and keeps the key', () => {
|
||||
let listened = false;
|
||||
const store = { publishFps: () => assert.fail('nothing to show it in') };
|
||||
const monitor = createFrameRateMonitor({
|
||||
viewer: { scene: { postRender: { addEventListener: () => () => {} } } },
|
||||
engine: { frames: { postRender: { addEventListener: () => () => {} } } },
|
||||
documentRef: {
|
||||
getElementById: () => null,
|
||||
addEventListener: () => (listened = true),
|
||||
@@ -130,11 +130,11 @@ test("destroying an earlier monitor leaves a newer one's readout", () => {
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
};
|
||||
const viewer = {
|
||||
scene: { postRender: { addEventListener: () => () => {} } },
|
||||
const engine = {
|
||||
frames: { postRender: { addEventListener: () => () => {} } },
|
||||
};
|
||||
const first = createFrameRateMonitor({ viewer, documentRef, store });
|
||||
const second = createFrameRateMonitor({ viewer, documentRef, store });
|
||||
const first = createFrameRateMonitor({ engine, documentRef, store });
|
||||
const second = createFrameRateMonitor({ engine, documentRef, store });
|
||||
const shown = store.fps;
|
||||
first.destroy();
|
||||
assert.ok(store.fps === shown);
|
||||
|
||||
@@ -19,7 +19,7 @@ test('the FPS readout sits in the title bar, hidden until the backquote key', as
|
||||
document.querySelector('#title-bar .frame-rate-readout');
|
||||
assert.equal(readout(), null, 'no readout without a monitor');
|
||||
const monitor = createFrameRateMonitor({
|
||||
viewer: { scene: { postRender: { addEventListener: () => () => {} } } },
|
||||
engine: { frames: { postRender: { addEventListener: () => () => {} } } },
|
||||
store: useStatusStore(pinia),
|
||||
});
|
||||
t.after(() => monitor.destroy());
|
||||
|
||||
+10
-11
@@ -82,17 +82,16 @@ export class LayerBindings {
|
||||
window,
|
||||
this._worldRequestFocusHandler,
|
||||
);
|
||||
// Cesium's trackedEntity changes announce navigation through this event.
|
||||
if (this.viewer)
|
||||
this._navigationOwnerChangedRemover =
|
||||
this.viewer.trackedEntityChanged.addEventListener((entity) => {
|
||||
// Rebinding the session's subject (a resumed renderer, a share
|
||||
// restore) is part of the pending restoration, not a new navigation
|
||||
// that supersedes it. The mark lasts only while the follow starts.
|
||||
if (entity && !this._disposed && !entity.gevRestoring)
|
||||
this._stampNavigation({ cancelPendingSelection: false });
|
||||
});
|
||||
// Vessel/installation focus flies without ever assigning a tracked entity,
|
||||
// A new follow announces navigation through this event.
|
||||
this._navigationOwnerChangedRemover =
|
||||
this.engine?.camera.followChanged.addEventListener((handle) => {
|
||||
// Rebinding the session's subject (a resumed renderer, a share
|
||||
// restore) is part of the pending restoration, not a new navigation
|
||||
// that supersedes it. The mark lasts only while the follow starts.
|
||||
if (handle && !this._disposed && !handle.native?.gevRestoring)
|
||||
this._stampNavigation({ cancelPendingSelection: false });
|
||||
}) ?? null;
|
||||
// Vessel/installation focus flies without ever starting a follow,
|
||||
// so it cannot reach the listener above. It announces instead.
|
||||
this._removeNavigationAuthorityListener =
|
||||
registerNavigationAuthorityListener(window, (event) => {
|
||||
|
||||
@@ -386,7 +386,6 @@ export class LocationNavigation {
|
||||
/* best-effort release */
|
||||
}
|
||||
this.engine.camera.following()?.stop();
|
||||
if (this.viewer) this.viewer.trackedEntity = undefined;
|
||||
this.engine.camera.cancelFlight();
|
||||
this.engine.camera.release();
|
||||
this._beginWorldJumpTransition();
|
||||
|
||||
@@ -185,7 +185,7 @@ export class NavigationController {
|
||||
} catch {
|
||||
/* best-effort release */
|
||||
}
|
||||
if (this.viewer) this.viewer.trackedEntity = undefined;
|
||||
this.engine?.camera.following()?.stop();
|
||||
this.interruptCameraMotion('explicit-navigation');
|
||||
this.stopOrbit();
|
||||
const camera = this.engine?.camera;
|
||||
|
||||
@@ -127,12 +127,12 @@ test('camera-entry listeners are removed before any asynchronous layer cleanup',
|
||||
const { owner } = bindings(t);
|
||||
let changes = 0;
|
||||
let trackedCallback;
|
||||
owner.viewer = { trackedEntityChanged: {
|
||||
owner.engine = { camera: { followChanged: {
|
||||
addEventListener(callback) {
|
||||
trackedCallback = callback;
|
||||
return () => { changes += 1; };
|
||||
},
|
||||
} };
|
||||
} } };
|
||||
owner._stampNavigation = () => { changes += 100; };
|
||||
owner.observeCamera();
|
||||
owner.stop();
|
||||
|
||||
@@ -344,7 +344,7 @@ export class ShellScene extends ShellFacade {
|
||||
this._locationNavigation.attachScene({ viewer, engine });
|
||||
this.hud.attachEngine(engine, { viewer });
|
||||
this.shareLinkManager.attachEngine(engine);
|
||||
this._displayBindings.attachViewer(viewer);
|
||||
this._displayBindings.attachEngine(engine);
|
||||
this._buildSceneControls();
|
||||
this._buildSceneCockpit();
|
||||
this._shareRestoration.attachLinks(this.shareLinkManager);
|
||||
@@ -441,7 +441,7 @@ export class ShellScene extends ShellFacade {
|
||||
if (this._orientationToken)
|
||||
useGlobeActionsStore(applicationPinia()).detach(this._orientationToken);
|
||||
this._orientationToken = null;
|
||||
this._displayBindings.attachViewer(null);
|
||||
this._displayBindings.attachEngine(null);
|
||||
this._locationNavigation.detachScene();
|
||||
this.hud.attachEngine(null);
|
||||
this.shareLinkManager.attachEngine(null);
|
||||
|
||||
@@ -28,7 +28,7 @@ function fakeShell() {
|
||||
},
|
||||
hud: { attachEngine: record('hud.attachEngine') },
|
||||
shareLink: { attachEngine: record('shareLink.attachEngine') },
|
||||
display: { attachViewer: record('display.attachViewer') },
|
||||
display: { attachEngine: record('display.attachEngine') },
|
||||
context: {
|
||||
canRetainScene: () => true,
|
||||
restoreForDisposal: record('context.restore'),
|
||||
@@ -124,7 +124,7 @@ test('a switch detaches the globe-bound parts and keeps the session objects', as
|
||||
'visual.destroy',
|
||||
'mapSource.destroy',
|
||||
'orientation.destroy',
|
||||
'display.attachViewer',
|
||||
'display.attachEngine',
|
||||
'location.detachScene',
|
||||
'hud.attachEngine',
|
||||
'shareLink.attachEngine',
|
||||
@@ -156,7 +156,7 @@ test('the next globe gets new globe-bound parts and the restore runs on it', asy
|
||||
'location.attachScene',
|
||||
'hud.attachEngine',
|
||||
'shareLink.attachEngine',
|
||||
'display.attachViewer',
|
||||
'display.attachEngine',
|
||||
'_buildSceneControls',
|
||||
'_buildSceneCockpit',
|
||||
'shareRestoration.attachLinks',
|
||||
|
||||
Reference in new issue
Block a user