mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
refactor: separate standalone Vite wiring and provider middleware
This commit is contained in:
1 parent
493acfc6aa
commit
8de1018c21
35 files changed
+8078
-7919
No files matched your search
@@ -0,0 +1,30 @@
|
||||
import { createApplication } from '../app/application.js';
|
||||
import { createStandaloneScene } from './scene.js';
|
||||
import { createStandaloneControls } from './controls.js';
|
||||
import { createStandaloneData } from './data.js';
|
||||
import { createStandaloneTools } from './tools.js';
|
||||
|
||||
// The existing controls and layer catalog contain page-scoped state.
|
||||
let constructed = false;
|
||||
|
||||
/** Compose the standalone application once per page. Reload to start again. */
|
||||
export function createStandaloneApplication({
|
||||
googleApiKey,
|
||||
cesiumToken,
|
||||
allowQaRegistration = false,
|
||||
}) {
|
||||
if (constructed)
|
||||
throw new Error('The standalone application already owns this page');
|
||||
constructed = true;
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
const loaderStatus = loadingScreen.querySelector('.loader-status');
|
||||
return createApplication({
|
||||
createScene: (context) =>
|
||||
createStandaloneScene({ ...context, googleApiKey, cesiumToken, loaderStatus }),
|
||||
createControls: (context) =>
|
||||
createStandaloneControls({ ...context, loaderStatus }),
|
||||
createData: (context) =>
|
||||
createStandaloneData({ ...context, allowQaRegistration }),
|
||||
createTools: (context) => createStandaloneTools({ ...context, loadingScreen }),
|
||||
});
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
import { StyleManager } from '../ui.js';
|
||||
import { flyToAustin } from '../camera.js';
|
||||
import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
|
||||
|
||||
/** Construct the existing controls and camera presentation. */
|
||||
export function createStandaloneControls({
|
||||
scene: { viewer, mapStackController },
|
||||
loaderStatus,
|
||||
defer,
|
||||
}) {
|
||||
// Initialize the style manager (post-processing, HUD, locations, share links)
|
||||
const styleManager = new StyleManager(viewer, { mapStackController });
|
||||
defer(() => styleManager.orbitController.stop());
|
||||
defer(() => styleManager.hud.destroy());
|
||||
defer(() => styleManager.dispose());
|
||||
// The previous multi-canvas weather compositor remains disabled. Cockpit
|
||||
// clouds use a separate, capped low-resolution GPU pass that never attaches
|
||||
// Cesium fog or post-process stages and is fully stopped in map mode.
|
||||
const weatherEffects = null;
|
||||
const cockpitCloudEffects = initCockpitCloudEffects(viewer);
|
||||
defer(() => cockpitCloudEffects?.destroy());
|
||||
|
||||
// If no share link state, do default fly-to Austin
|
||||
if (!styleManager.hasShareState) {
|
||||
loaderStatus.textContent = 'Flying to Austin, TX...';
|
||||
defer(flyToAustin(viewer));
|
||||
} else {
|
||||
loaderStatus.textContent = 'Restoring shared view...';
|
||||
}
|
||||
|
||||
return { styleManager, weatherEffects, cockpitCloudEffects };
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { DataLayerManager } from '../data/manager.js';
|
||||
import flightsLayer from '../data/flights.js';
|
||||
import militaryFlightsLayer from '../data/militaryFlights.js';
|
||||
import earthquakesLayer from '../data/earthquakes.js';
|
||||
import satellitesLayer from '../data/satellites.js';
|
||||
import rocketLaunchesLayer from '../data/rocketLaunches.js';
|
||||
import trafficLayer from '../data/traffic.js';
|
||||
import cctvLayer from '../data/cctv.js';
|
||||
import radioLayer from '../data/radio.js';
|
||||
import bikeshareLayer from '../data/bikeshare.js';
|
||||
import aisLiveVesselsLayer from '../data/aisLiveVessels.js';
|
||||
import militaryInstallationsLayer from '../data/militaryInstallations.js';
|
||||
import militaryAwarenessLayer from '../data/militaryAwareness.js';
|
||||
import localDataLayers from '../data/localLayers.js';
|
||||
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
||||
|
||||
/** Register the standalone layer catalog before allowing state restoration. */
|
||||
export function createStandaloneData({
|
||||
scene: { viewer },
|
||||
controls: { styleManager },
|
||||
allowQaRegistration,
|
||||
defer,
|
||||
}) {
|
||||
// Initialize data layer manager
|
||||
const dataManager = new DataLayerManager(viewer, {
|
||||
allowQaRegistration,
|
||||
});
|
||||
defer(async () => {
|
||||
await dataManager.destroyAll();
|
||||
if (dataManager.layers.size)
|
||||
throw new Error(
|
||||
`Data layers could not be destroyed: ${[...dataManager.layers.keys()].join(', ')}`,
|
||||
);
|
||||
});
|
||||
dataManager.register(flightsLayer);
|
||||
dataManager.register(militaryFlightsLayer);
|
||||
dataManager.register(earthquakesLayer);
|
||||
dataManager.register(satellitesLayer);
|
||||
dataManager.register(rocketLaunchesLayer);
|
||||
rocketLaunchesLayer.attachDataManager(dataManager);
|
||||
dataManager.register(trafficLayer);
|
||||
dataManager.register(cctvLayer);
|
||||
dataManager.register(radioLayer);
|
||||
dataManager.register(bikeshareLayer);
|
||||
dataManager.register(aisLiveVesselsLayer);
|
||||
dataManager.register(militaryInstallationsLayer);
|
||||
dataManager.register(militaryAwarenessLayer);
|
||||
militaryAwarenessLayer.attachDataManager(dataManager);
|
||||
for (const layer of localDataLayers) {
|
||||
dataManager.register(layer);
|
||||
}
|
||||
// Restoration starts only after the complete production registry is sealed.
|
||||
dataManager.finalizeRegistrations(LAYER_STATE_REGISTRY);
|
||||
if (allowQaRegistration) {
|
||||
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
||||
if (targetManager !== dataManager)
|
||||
throw new Error('QA layer manager mismatch');
|
||||
return dataManager.registerForQa(layerModule);
|
||||
};
|
||||
window.__gevQaUnregisterLayer = (targetManager, layerId) => {
|
||||
if (targetManager !== dataManager)
|
||||
throw new Error('QA layer manager mismatch');
|
||||
return dataManager.unregisterForQa(layerId);
|
||||
};
|
||||
const register = window.__gevQaRegisterLayer;
|
||||
const unregister = window.__gevQaUnregisterLayer;
|
||||
defer(() => {
|
||||
if (window.__gevQaRegisterLayer === register)
|
||||
delete window.__gevQaRegisterLayer;
|
||||
if (window.__gevQaUnregisterLayer === unregister)
|
||||
delete window.__gevQaUnregisterLayer;
|
||||
});
|
||||
}
|
||||
dataManager.buildTogglePanel(document.getElementById('data-toggles'));
|
||||
styleManager.attachDataManager(dataManager);
|
||||
|
||||
return { dataManager };
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Extract a human-readable error message from any thrown value.
|
||||
* Handles Error objects, strings, and plain objects with message/error fields.
|
||||
* @param {*} error — caught exception value
|
||||
* @returns {string} best-effort error description
|
||||
*/
|
||||
export function describeError(error) {
|
||||
if (!error) return 'Unknown initialization error';
|
||||
if (error instanceof Error) {
|
||||
if (error.message && error.message.trim()) return error.message.trim();
|
||||
return error.name || 'Initialization error';
|
||||
}
|
||||
if (typeof error === 'string' && error.trim()) return error.trim();
|
||||
if (typeof error === 'object') {
|
||||
const maybeMessage = String(error.message || error.error || '').trim();
|
||||
if (maybeMessage) return maybeMessage;
|
||||
try {
|
||||
const serialized = JSON.stringify(error);
|
||||
if (serialized && serialized !== '{}') return serialized;
|
||||
} catch {
|
||||
// ignore serialization error
|
||||
}
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { createApplicationViewer } from '../app/viewer.js';
|
||||
import { registerDataCredits } from '../data/dataCredits.js';
|
||||
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
|
||||
import { MapStackController } from '../mapStackController.js';
|
||||
import { loadPhotorealisticTileset } from '../mapStartup.js';
|
||||
import { initLogoGaze } from '../logoGaze.js';
|
||||
import { uninstallRenderGovernor } from '../renderGovernor.js';
|
||||
import { describeError } from './errors.js';
|
||||
|
||||
/** Construct the standalone globe using the caller's local configuration. */
|
||||
export async function createStandaloneScene({
|
||||
googleApiKey,
|
||||
cesiumToken,
|
||||
loaderStatus,
|
||||
signal,
|
||||
defer,
|
||||
}) {
|
||||
defer(initLogoGaze());
|
||||
const previousKey = window.__GOOGLE_MAPS_API_KEY__;
|
||||
if (googleApiKey) {
|
||||
window.__GOOGLE_MAPS_API_KEY__ = googleApiKey;
|
||||
defer(() => {
|
||||
if (window.__GOOGLE_MAPS_API_KEY__ !== googleApiKey) return;
|
||||
if (previousKey === undefined) delete window.__GOOGLE_MAPS_API_KEY__;
|
||||
else window.__GOOGLE_MAPS_API_KEY__ = previousKey;
|
||||
});
|
||||
}
|
||||
loaderStatus.textContent = 'Configuring viewer...';
|
||||
// Provider attribution stays visible, including clean-view and recording.
|
||||
const creditContainer = document.createElement('div');
|
||||
creditContainer.id = 'cesium-credits';
|
||||
document.body.appendChild(creditContainer);
|
||||
defer(() => creditContainer.remove());
|
||||
const viewer = createApplicationViewer({
|
||||
container: 'cesiumContainer',
|
||||
creditContainer,
|
||||
});
|
||||
defer(() => {
|
||||
uninstallRenderGovernor(viewer);
|
||||
if (!viewer.isDestroyed()) viewer.destroy();
|
||||
});
|
||||
registerDataCredits(viewer);
|
||||
configureCreditKeyboardAccess(document);
|
||||
loaderStatus.textContent =
|
||||
googleApiKey || cesiumToken
|
||||
? 'Loading Google 3D Tiles...'
|
||||
: 'Loading the keyless globe...';
|
||||
const photoreal = await loadPhotorealisticTileset(Cesium, {
|
||||
googleApiKey,
|
||||
cesiumToken,
|
||||
});
|
||||
const tileset = photoreal.tileset;
|
||||
// A provider can finish after cancellation; retain ownership of its result.
|
||||
defer(() => {
|
||||
if (tileset && !tileset.isDestroyed()) {
|
||||
if (!viewer.scene.primitives.remove(tileset)) tileset.destroy();
|
||||
}
|
||||
});
|
||||
signal.throwIfAborted();
|
||||
if (tileset) {
|
||||
viewer.scene.primitives.add(tileset);
|
||||
// NOTE: Cesium World Terrain intentionally disabled — conflicts with Google 3D Tiles at high zoom.
|
||||
// Google Photorealistic 3D Tiles provide their own terrain/elevation.
|
||||
viewer.scene.globe.show = false;
|
||||
console.info(`[Init] Google 3D Tiles loaded via ${photoreal.route}.`);
|
||||
} else {
|
||||
if (photoreal.errors.length) {
|
||||
const tileError = photoreal.errors.at(-1);
|
||||
console.warn(
|
||||
'[Init] Google 3D Tiles unavailable, using the keyless globe:',
|
||||
tileError,
|
||||
);
|
||||
const tileErrorDetail = describeError(tileError);
|
||||
loaderStatus.textContent = `Google 3D Tiles unavailable (${tileErrorDetail}). Loading the keyless globe...`;
|
||||
}
|
||||
viewer.scene.globe.show = true;
|
||||
}
|
||||
|
||||
loaderStatus.textContent = 'Initializing systems...';
|
||||
|
||||
const mapStackController = new MapStackController(viewer, {
|
||||
googleTileset: tileset,
|
||||
cesiumToken,
|
||||
initialStack: tileset ? 'photoreal' : 'esri-imagery',
|
||||
// Task 5 (height-datum fix): rebroadcast stack changes as a window
|
||||
// CustomEvent so data layers (CCTV per-regime ground resolution) can
|
||||
// react without coupling MapStackController to layer modules. Fires on
|
||||
// 'switching'/'ready'/'error'; listeners derive the surface regime from
|
||||
// live scene state, so intermediate emissions are harmless.
|
||||
onChange: (state) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('gev:map-stack-changed', { detail: state }),
|
||||
);
|
||||
},
|
||||
onError: (message) => console.warn('[MapStack]', message),
|
||||
});
|
||||
defer(() => mapStackController.destroy());
|
||||
await mapStackController.setStack(tileset ? 'photoreal' : 'esri-imagery', {
|
||||
silent: true,
|
||||
});
|
||||
|
||||
signal.throwIfAborted();
|
||||
return { viewer, tileset, mapStackController };
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { initFirstRunExperience } from '../firstRunExperience.js';
|
||||
import { initKeySetup } from '../keySetup.js';
|
||||
|
||||
/** Reveal welcome controls only after restoration and the loading transition. */
|
||||
export function startStandaloneChrome({
|
||||
loadingScreen,
|
||||
styleManager,
|
||||
dataManager,
|
||||
signal,
|
||||
}) {
|
||||
let disposed = false;
|
||||
let firstRun;
|
||||
let revealTimer;
|
||||
let resolveDelay;
|
||||
const minimumDelay = new Promise((resolve) => {
|
||||
resolveDelay = resolve;
|
||||
});
|
||||
const delayTimer = setTimeout(resolveDelay, 1000);
|
||||
const revealFirstRun = () => {
|
||||
if (disposed || signal.aborted || firstRun) return;
|
||||
firstRun = initFirstRunExperience({ styleManager, dataManager });
|
||||
clearTimeout(revealTimer);
|
||||
loadingScreen.removeEventListener('transitionend', revealFirstRun);
|
||||
};
|
||||
void Promise.all([styleManager.initialRestorePromise, minimumDelay])
|
||||
.catch(() => {
|
||||
/* Restoration reports its own outcome through the controls. */
|
||||
})
|
||||
.then(() => {
|
||||
if (disposed || signal.aborted) return;
|
||||
loadingScreen.classList.add('hidden');
|
||||
loadingScreen.addEventListener('transitionend', revealFirstRun, {
|
||||
once: true,
|
||||
});
|
||||
revealTimer = setTimeout(revealFirstRun, 900);
|
||||
});
|
||||
const keySetup = initKeySetup({ signal });
|
||||
// Own the pending initializer too; it must not reveal a dialog after abort.
|
||||
void keySetup.catch(() =>
|
||||
console.error('Provider settings initialization failed'),
|
||||
);
|
||||
return async () => {
|
||||
disposed = true;
|
||||
clearTimeout(delayTimer);
|
||||
clearTimeout(revealTimer);
|
||||
resolveDelay();
|
||||
loadingScreen.removeEventListener('transitionend', revealFirstRun);
|
||||
firstRun?.destroy();
|
||||
(await keySetup.catch(() => null))?.destroy();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const source = readFileSync(
|
||||
new URL('./startupChrome.js', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
.replace(/^import .*;\n/gm, '')
|
||||
.replace('export function', 'function');
|
||||
|
||||
function fixture() {
|
||||
const timers = new Map();
|
||||
const listeners = new Map();
|
||||
const events = [];
|
||||
let restored;
|
||||
let nextTimer = 0;
|
||||
const controller = new AbortController();
|
||||
const context = {
|
||||
console,
|
||||
setTimeout(fn, delay) {
|
||||
const id = ++nextTimer;
|
||||
timers.set(id, { fn, delay });
|
||||
return id;
|
||||
},
|
||||
clearTimeout(id) {
|
||||
timers.delete(id);
|
||||
},
|
||||
initFirstRunExperience() {
|
||||
events.push('welcome');
|
||||
return { destroy: () => events.push('welcome:destroy') };
|
||||
},
|
||||
async initKeySetup() {
|
||||
return { destroy: () => events.push('settings:destroy') };
|
||||
},
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(source, context);
|
||||
const stop = context.startStandaloneChrome({
|
||||
loadingScreen: {
|
||||
classList: { add: (value) => events.push(value) },
|
||||
addEventListener: (type, listener) => listeners.set(type, listener),
|
||||
removeEventListener: (type) => listeners.delete(type),
|
||||
},
|
||||
styleManager: {
|
||||
initialRestorePromise: new Promise((resolve) => {
|
||||
restored = resolve;
|
||||
}),
|
||||
},
|
||||
dataManager: {},
|
||||
signal: controller.signal,
|
||||
});
|
||||
return {
|
||||
events,
|
||||
listeners,
|
||||
stop,
|
||||
restored,
|
||||
controller,
|
||||
fire(delay) {
|
||||
for (const [id, task] of timers) {
|
||||
if (task.delay === delay) {
|
||||
timers.delete(id);
|
||||
task.fn();
|
||||
}
|
||||
}
|
||||
},
|
||||
timers,
|
||||
};
|
||||
}
|
||||
async function flush() {
|
||||
for (let i = 0; i < 12; i++) await Promise.resolve();
|
||||
}
|
||||
|
||||
test('welcome waits for restoration, minimum delay, and the cover transition', async () => {
|
||||
const f = fixture();
|
||||
f.fire(1000);
|
||||
await flush();
|
||||
assert.deepEqual(f.events, []);
|
||||
f.restored();
|
||||
await flush();
|
||||
assert.deepEqual(f.events, ['hidden']);
|
||||
f.listeners.get('transitionend')();
|
||||
f.fire(900);
|
||||
assert.deepEqual(f.events, ['hidden', 'welcome']);
|
||||
await f.stop();
|
||||
assert.deepEqual(f.events.slice(-2), ['welcome:destroy', 'settings:destroy']);
|
||||
assert.equal(f.timers.size, 0);
|
||||
});
|
||||
|
||||
test('reduced motion uses the bounded fallback after the cover hides', async () => {
|
||||
const f = fixture();
|
||||
f.restored();
|
||||
f.fire(1000);
|
||||
await flush();
|
||||
f.fire(900);
|
||||
assert.deepEqual(f.events, ['hidden', 'welcome']);
|
||||
await f.stop();
|
||||
});
|
||||
|
||||
test('shutdown while restore is pending never reveals late welcome UI', async () => {
|
||||
const f = fixture();
|
||||
f.controller.abort();
|
||||
await f.stop();
|
||||
f.restored();
|
||||
await flush();
|
||||
f.fire(1000);
|
||||
f.fire(900);
|
||||
assert.deepEqual(f.events, ['settings:destroy']);
|
||||
assert.equal(f.listeners.size, 0);
|
||||
assert.equal(f.timers.size, 0);
|
||||
});
|
||||
|
||||
test('shutdown during the cover transition cancels the listener and fallback', async () => {
|
||||
const f = fixture();
|
||||
f.restored();
|
||||
f.fire(1000);
|
||||
await flush();
|
||||
const transition = f.listeners.get('transitionend');
|
||||
f.controller.abort();
|
||||
await f.stop();
|
||||
transition();
|
||||
f.fire(900);
|
||||
assert.deepEqual(f.events, ['hidden', 'settings:destroy']);
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { SceneDirector } from '../scenes/director.js';
|
||||
import { initAnnotations } from '../annotations/index.js';
|
||||
import { initGevVoiceCommands } from '../voice/gevRealtime.js';
|
||||
import { installScopeMask, destroyScopeMask } from '../scopeMask.js';
|
||||
import {
|
||||
installRenderGovernor,
|
||||
getRenderGovernorDiagnostics,
|
||||
governorRequestRender,
|
||||
holdContinuousRender,
|
||||
releaseContinuousRender,
|
||||
} from '../renderGovernor.js';
|
||||
import { startStandaloneChrome } from './startupChrome.js';
|
||||
|
||||
/** Attach scene tools, rendering listeners and the standalone debug handle. */
|
||||
export function createStandaloneTools({
|
||||
scene,
|
||||
controls,
|
||||
data,
|
||||
loadingScreen,
|
||||
signal,
|
||||
defer,
|
||||
}) {
|
||||
const { viewer, tileset, mapStackController } = scene;
|
||||
const { styleManager, weatherEffects, cockpitCloudEffects } = controls;
|
||||
const { dataManager } = data;
|
||||
const sceneDirector = new SceneDirector(viewer, styleManager, dataManager);
|
||||
defer(() => sceneDirector.destroy());
|
||||
const annotations = initAnnotations({ viewer, tileset });
|
||||
defer(() => {
|
||||
if (window.__gevAnnotations === annotations) delete window.__gevAnnotations;
|
||||
annotations.destroy();
|
||||
});
|
||||
defer(startStandaloneChrome({ loadingScreen, styleManager, dataManager, signal }));
|
||||
// Idle render governor: flips the scene into requestRenderMode whenever
|
||||
// nothing animates per frame. Installed AFTER every module above has had
|
||||
// its chance to register pre-install holds. (perf wave 2)
|
||||
installRenderGovernor(viewer);
|
||||
|
||||
// Install the explicit scope mask used by the DISPLAY controls.
|
||||
installScopeMask(viewer);
|
||||
defer(() => destroyScopeMask());
|
||||
|
||||
// The follow camera recomputes the tracked target's dead-reckon position
|
||||
// every frame — tracking anything is a per-frame animation. (perf wave 2)
|
||||
const removeTrackingListener = viewer.trackedEntityChanged.addEventListener(
|
||||
() => {
|
||||
if (viewer.trackedEntity) holdContinuousRender('tracked-entity');
|
||||
else releaseContinuousRender('tracked-entity');
|
||||
},
|
||||
);
|
||||
|
||||
// Hidden-state suspension (perf wave 2): when the window/tab is hidden,
|
||||
// stop the default render loop outright — a hidden canvas repaints for
|
||||
// nobody, and browser rAF throttling still lets throttled frames burn
|
||||
// GPU. Holder/data state is untouched, so return is seamless: restore
|
||||
// the loop, refresh the one DOM surface we gated, render a frame.
|
||||
const syncVisibilitySuspension = () => {
|
||||
const hidden = document.hidden;
|
||||
viewer.useDefaultRenderLoop = !hidden;
|
||||
cockpitCloudEffects?.setSuspended?.(hidden);
|
||||
if (!hidden) {
|
||||
if (dataManager._panelRefreshPendingOnVisible) {
|
||||
dataManager._panelRefreshPendingOnVisible = false;
|
||||
dataManager._refreshTogglePanel();
|
||||
}
|
||||
governorRequestRender('visibility-restore');
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', syncVisibilitySuspension);
|
||||
defer(() =>
|
||||
document.removeEventListener('visibilitychange', syncVisibilitySuspension),
|
||||
);
|
||||
defer(() => {
|
||||
removeTrackingListener();
|
||||
releaseContinuousRender('tracked-entity');
|
||||
});
|
||||
// Apply the CURRENT state too — bootstrap can complete while the tab is
|
||||
// already hidden, and waiting for the next transition would leave the
|
||||
// loop burning behind a hidden tab. (perf wave 2 fix)
|
||||
syncVisibilitySuspension();
|
||||
|
||||
window.__godsEyeView = {
|
||||
viewer,
|
||||
styleManager,
|
||||
tileset,
|
||||
dataManager,
|
||||
sceneDirector,
|
||||
mapStackController,
|
||||
annotations,
|
||||
weatherEffects,
|
||||
cockpitCloudEffects,
|
||||
getRenderGovernorDiagnostics,
|
||||
requestRender: governorRequestRender,
|
||||
};
|
||||
const debug = window.__godsEyeView;
|
||||
defer(() => {
|
||||
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
||||
});
|
||||
const voiceCommands = initGevVoiceCommands({
|
||||
viewer,
|
||||
styleManager,
|
||||
dataManager,
|
||||
sceneDirector,
|
||||
annotations,
|
||||
});
|
||||
defer(() => {
|
||||
voiceCommands.stop({ removeUi: true });
|
||||
if (window.__gevVoiceCommands === voiceCommands)
|
||||
delete window.__gevVoiceCommands;
|
||||
});
|
||||
debug.voiceCommands = voiceCommands;
|
||||
return { sceneDirector, annotations, voiceCommands };
|
||||
}
|
||||
Reference in new issue
Block a user