mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Attach the tools, data and controls bound to one globe as scene parts
Each phase builds its session objects once (shell, layer manager and presentation, Director, annotation board, Draw, Import, voice, debug handle) and registers the parts bound to one globe: the shell's later attachments, Cockpit clouds, the catalog binding and sonar, the Director's scene, the Cesium or Navara tools and the debug handle's globe fields. A page with a scene host attaches them to every engine; a host that never switches attaches them once, as before. The shell releases its globe before the data layers detach, and the Navara notice goes with its engine.
This commit is contained in:
1 parent
061ed1b682
commit
2bac200c01
8 files changed
+410
-148
No files matched your search
@@ -2661,6 +2661,7 @@
|
|||||||
"src/app/surfaceServices.js",
|
"src/app/surfaceServices.js",
|
||||||
"src/app/tools.js",
|
"src/app/tools.js",
|
||||||
"src/app/sessionVoice.js",
|
"src/app/sessionVoice.js",
|
||||||
|
"src/app/sceneParts.js",
|
||||||
"src/app/viewer.js",
|
"src/app/viewer.js",
|
||||||
"src/sky/starrySky.js",
|
"src/sky/starrySky.js",
|
||||||
"src/sky/stars.js",
|
"src/sky/stars.js",
|
||||||
|
|||||||
Vendored
+63
-10
@@ -3,15 +3,20 @@ import { StyleManager } from '../ui/composition.js';
|
|||||||
import { flyToAustin } from '../camera.js';
|
import { flyToAustin } from '../camera.js';
|
||||||
import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
|
import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
|
||||||
import { t } from '../i18n/index.js';
|
import { t } from '../i18n/index.js';
|
||||||
|
import { attachScenePart, currentSceneOf } from './sceneParts.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct the existing controls and camera presentation. `initialShareCode`
|
* Construct the existing controls and camera presentation. `initialShareCode`
|
||||||
* opens a share code (the `<code>` of `/share/<code>`) at start-up, for a host
|
* opens a share code (the `<code>` of `/share/<code>`) at start-up, for a host
|
||||||
* whose address does not carry one. `localePicker: false` removes the language
|
* whose address does not carry one. `localePicker: false` removes the language
|
||||||
* picker, for a host that does not initialize Core's locale.
|
* picker, for a host that does not initialize Core's locale.
|
||||||
|
*
|
||||||
|
* The shell is built once, on the first globe; on a page that switches
|
||||||
|
* engines it attaches to each later one (StyleManager.attachScene) with that
|
||||||
|
* engine's catalog, and the Cockpit cloud effects follow the viewer.
|
||||||
*/
|
*/
|
||||||
export function createApplicationControls({
|
export function createApplicationControls({
|
||||||
scene: { viewer, engine, mapStackController, operations },
|
scene,
|
||||||
loaderStatus,
|
loaderStatus,
|
||||||
Controls = StyleManager,
|
Controls = StyleManager,
|
||||||
services,
|
services,
|
||||||
@@ -23,14 +28,18 @@ export function createApplicationControls({
|
|||||||
engineSwitch = false,
|
engineSwitch = false,
|
||||||
layerSession,
|
layerSession,
|
||||||
}) {
|
}) {
|
||||||
// Initialize the style manager (post-processing, HUD, locations, share links)
|
const { viewer, engine, mapStackController, operations } = scene;
|
||||||
const styleManager = new Controls(viewer, {
|
const sharedServices = {
|
||||||
services: {
|
|
||||||
...services,
|
...services,
|
||||||
...operations.surface.controlServices,
|
...operations.surface.controlServices,
|
||||||
searchAndFlyTo: operations.searchAndFlyTo,
|
searchAndFlyTo: operations.searchAndFlyTo,
|
||||||
fetchRegionalBrief: (...args) =>
|
fetchRegionalBrief: (...args) =>
|
||||||
operations.requests.regional.getBrief(...args),
|
operations.requests.regional.getBrief(...args),
|
||||||
|
};
|
||||||
|
// Initialize the style manager (post-processing, HUD, locations, share links)
|
||||||
|
const styleManager = new Controls(viewer, {
|
||||||
|
services: {
|
||||||
|
...sharedServices,
|
||||||
// The catalog's layers; the ones this engine cannot show (Navara)
|
// The catalog's layers; the ones this engine cannot show (Navara)
|
||||||
// are empty stand-ins.
|
// are empty stand-ins.
|
||||||
...catalogControlServices(catalog),
|
...catalogControlServices(catalog),
|
||||||
@@ -49,16 +58,54 @@ export function createApplicationControls({
|
|||||||
defer(() => styleManager.orbitController.stop());
|
defer(() => styleManager.orbitController.stop());
|
||||||
defer(() => styleManager.hud.destroy());
|
defer(() => styleManager.hud.destroy());
|
||||||
defer(() => styleManager.dispose());
|
defer(() => styleManager.dispose());
|
||||||
|
// The shell was built on this globe; later globes attach to it.
|
||||||
|
let builtOn = currentSceneOf(scene);
|
||||||
|
attachScenePart(
|
||||||
|
scene,
|
||||||
|
{
|
||||||
|
name: 'shell',
|
||||||
|
attachScene(next) {
|
||||||
|
if (next === builtOn) {
|
||||||
|
builtOn = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
builtOn = null;
|
||||||
|
styleManager.attachScene({
|
||||||
|
viewer: next.viewer,
|
||||||
|
engine: next.engine,
|
||||||
|
mapStackController: next.mapStackController,
|
||||||
|
services: catalogControlServices(next.catalog ?? catalog),
|
||||||
|
engineSwitch: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// The shell leaves its globe in 'shell-release' (the tools phase),
|
||||||
|
// before the data layers detach, as its teardown always did.
|
||||||
|
},
|
||||||
|
{ defer },
|
||||||
|
);
|
||||||
// The previous multi-canvas weather compositor remains disabled. Cockpit
|
// The previous multi-canvas weather compositor remains disabled. Cockpit
|
||||||
// clouds use a separate, capped low-resolution GPU pass that never attaches
|
// 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.
|
// Cesium fog or post-process stages and is fully stopped in map mode.
|
||||||
const weatherEffects = null;
|
const weatherEffects = null;
|
||||||
const cockpitCloudEffects = viewer
|
let cockpitCloudEffects = null;
|
||||||
? initCockpitCloudEffects(viewer, {
|
attachScenePart(
|
||||||
|
scene,
|
||||||
|
{
|
||||||
|
name: 'cockpit-clouds',
|
||||||
|
attachScene(next, { defer: release }) {
|
||||||
|
if (!next.viewer) return;
|
||||||
|
const effects = initCockpitCloudEffects(next.viewer, {
|
||||||
weatherService: operations.requests.weather,
|
weatherService: operations.requests.weather,
|
||||||
})
|
});
|
||||||
: null;
|
cockpitCloudEffects = effects;
|
||||||
defer(() => cockpitCloudEffects?.destroy());
|
release(() => {
|
||||||
|
if (cockpitCloudEffects === effects) cockpitCloudEffects = null;
|
||||||
|
effects?.destroy();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ defer },
|
||||||
|
);
|
||||||
|
|
||||||
// If no share link state, do default fly-to Austin
|
// If no share link state, do default fly-to Austin
|
||||||
if (!styleManager.hasShareState) {
|
if (!styleManager.hasShareState) {
|
||||||
@@ -68,5 +115,11 @@ export function createApplicationControls({
|
|||||||
loaderStatus.textContent = t('Restoring shared view...');
|
loaderStatus.textContent = t('Restoring shared view...');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { styleManager, weatherEffects, cockpitCloudEffects };
|
return {
|
||||||
|
styleManager,
|
||||||
|
weatherEffects,
|
||||||
|
get cockpitCloudEffects() {
|
||||||
|
return cockpitCloudEffects;
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -13,9 +13,14 @@ test('the controls pass the scene engine to the shell', () => {
|
|||||||
const controls = read('./controls.js');
|
const controls = read('./controls.js');
|
||||||
assert.match(
|
assert.match(
|
||||||
controls,
|
controls,
|
||||||
/scene: \{ viewer, engine, mapStackController, operations \}/,
|
/const \{ viewer, engine, mapStackController, operations \} = scene;/,
|
||||||
);
|
);
|
||||||
assert.match(controls, /new Controls\(viewer, \{[\s\S]*?\n\s+engine,\n/);
|
assert.match(controls, /new Controls\(viewer, \{[\s\S]*?\n\s+engine,\n/);
|
||||||
|
// A later globe reaches the shell with its own engine.
|
||||||
|
assert.match(
|
||||||
|
controls,
|
||||||
|
/styleManager\.attachScene\(\{\s+viewer: next\.viewer,\s+engine: next\.engine,/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the shell initializes the world overlay on its engine', () => {
|
test('the shell initializes the world overlay on its engine', () => {
|
||||||
|
|||||||
+78
-33
@@ -2,9 +2,17 @@ import { LayerLifecycle } from '../data/lifecycle.js';
|
|||||||
import { LayerPresentation } from './layerPresentation.js';
|
import { LayerPresentation } from './layerPresentation.js';
|
||||||
import { createCyberSonarScene } from '../cyberSonarScene.js';
|
import { createCyberSonarScene } from '../cyberSonarScene.js';
|
||||||
import { panelAwareVisibility } from './embed.js';
|
import { panelAwareVisibility } from './embed.js';
|
||||||
/** Register the application layer catalog before allowing state restoration. */
|
import { attachScenePart, currentSceneOf } from './sceneParts.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the application layer catalog before allowing state restoration.
|
||||||
|
* The layer manager and presentation belong to the application (to the page
|
||||||
|
* session when there is one); each globe binds its catalog to them, and an
|
||||||
|
* engine switch only detaches the layers' drawing (LayerLifecycle
|
||||||
|
* detachScene).
|
||||||
|
*/
|
||||||
export function createApplicationData({
|
export function createApplicationData({
|
||||||
scene: { viewer, engine, mapStackController },
|
scene,
|
||||||
controls: { styleManager },
|
controls: { styleManager },
|
||||||
catalog,
|
catalog,
|
||||||
allowQaRegistration,
|
allowQaRegistration,
|
||||||
@@ -13,12 +21,13 @@ export function createApplicationData({
|
|||||||
defer,
|
defer,
|
||||||
signal = null,
|
signal = null,
|
||||||
}) {
|
}) {
|
||||||
|
const first = currentSceneOf(scene);
|
||||||
// Initialize data layer manager
|
// Initialize data layer manager
|
||||||
// The modules get { signal, engine } on every call (viewer null on Navara).
|
// The modules get { signal, engine } on every call (viewer null on Navara).
|
||||||
const dataManager =
|
const dataManager =
|
||||||
layerSession?.manager ??
|
layerSession?.manager ??
|
||||||
new LayerLifecycle(viewer, {
|
new LayerLifecycle(first.viewer, {
|
||||||
engine,
|
engine: first.engine,
|
||||||
allowQaRegistration,
|
allowQaRegistration,
|
||||||
// A panel's host may report it hidden while it is on screen; its layers
|
// A panel's host may report it hidden while it is on screen; its layers
|
||||||
// keep polling (see keepPanelRendering in embed.js).
|
// keep polling (see keepPanelRendering in embed.js).
|
||||||
@@ -26,11 +35,10 @@ export function createApplicationData({
|
|||||||
getSourceAvailability: catalog?.getSourceAvailability,
|
getSourceAvailability: catalog?.getSourceAvailability,
|
||||||
});
|
});
|
||||||
if (layerSession) layerSession.manager = dataManager;
|
if (layerSession) layerSession.manager = dataManager;
|
||||||
|
const sessionGoes = () =>
|
||||||
|
!layerSession || layerSession.lifetime.signal.aborted;
|
||||||
defer(async () => {
|
defer(async () => {
|
||||||
if (layerSession && !layerSession.lifetime.signal.aborted) {
|
if (!sessionGoes()) return;
|
||||||
await dataManager.detachScene();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dataManager.destroyAll();
|
await dataManager.destroyAll();
|
||||||
if (dataManager.layers.size)
|
if (dataManager.layers.size)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -44,19 +52,11 @@ export function createApplicationData({
|
|||||||
});
|
});
|
||||||
if (layerSession) layerSession.presentation = presentation;
|
if (layerSession) layerSession.presentation = presentation;
|
||||||
defer(() => {
|
defer(() => {
|
||||||
if (layerSession && !layerSession.lifetime.signal.aborted)
|
if (sessionGoes()) presentation.destroy();
|
||||||
presentation.suspend();
|
|
||||||
else presentation.destroy();
|
|
||||||
});
|
});
|
||||||
onData?.(dataManager);
|
onData?.(dataManager);
|
||||||
if (!catalog?.layers || !catalog?.metadata)
|
if (!catalog?.layers || !catalog?.metadata)
|
||||||
throw new TypeError('An application layer catalog is required');
|
throw new TypeError('An application layer catalog is required');
|
||||||
for (const layer of catalog.layers)
|
|
||||||
layer.attachMapStackController?.(mapStackController);
|
|
||||||
const attached = dataManager.bindCatalog(viewer, engine, catalog, {
|
|
||||||
waitForCameraRestore: layerSession?.restoringView === true,
|
|
||||||
});
|
|
||||||
for (const layer of catalog.layers) layer.attachDataManager?.(dataManager);
|
|
||||||
|
|
||||||
if (allowQaRegistration) {
|
if (allowQaRegistration) {
|
||||||
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
||||||
@@ -78,31 +78,76 @@ export function createApplicationData({
|
|||||||
delete window.__gevQaUnregisterLayer;
|
delete window.__gevQaUnregisterLayer;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Each globe binds its catalog: the layers' drawing, the Data panel and
|
||||||
|
// the shell's layer bindings follow the engine.
|
||||||
|
let current = catalog;
|
||||||
|
const attached = attachScenePart(
|
||||||
|
scene,
|
||||||
|
{
|
||||||
|
name: 'data',
|
||||||
|
attachScene(next, { signal: attachment, defer: release }) {
|
||||||
|
const nextCatalog = next.catalog ?? catalog;
|
||||||
|
current = nextCatalog;
|
||||||
|
for (const layer of nextCatalog.layers)
|
||||||
|
layer.attachMapStackController?.(next.mapStackController);
|
||||||
|
const binding = dataManager.bindCatalog(
|
||||||
|
next.viewer,
|
||||||
|
next.engine,
|
||||||
|
nextCatalog,
|
||||||
|
{ waitForCameraRestore: layerSession?.restoringView === true },
|
||||||
|
);
|
||||||
|
for (const layer of nextCatalog.layers)
|
||||||
|
layer.attachDataManager?.(dataManager);
|
||||||
presentation.mount(
|
presentation.mount(
|
||||||
document.getElementById('data-toggles'),
|
document.getElementById('data-toggles'),
|
||||||
catalog.weatherClock,
|
nextCatalog.weatherClock,
|
||||||
);
|
);
|
||||||
styleManager.attachDataManager(dataManager);
|
styleManager.attachDataManager(dataManager);
|
||||||
// The sonar scene draws on the viewer; the Navara engine has none.
|
// The sonar scene draws on the viewer; the Navara engine has none.
|
||||||
if (viewer) defer(createCyberSonarScene(viewer, dataManager));
|
if (next.viewer)
|
||||||
|
release(createCyberSonarScene(next.viewer, dataManager));
|
||||||
const data = { dataManager, catalog, presentation };
|
if (!binding) return;
|
||||||
if (!attached) return data;
|
// A teardown or switch during the attachment must not wait for slow
|
||||||
// A teardown during startup must not wait for slow layer attachments: it
|
// layer attachments: it ends this wait, and the detach releases the
|
||||||
// ends this wait, and the data cleanup above releases the scene.
|
// scene.
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const onAbort = () => resolve(data);
|
const waits = [signal, attachment].filter(Boolean);
|
||||||
if (signal?.aborted) onAbort();
|
const onAbort = () => {
|
||||||
else signal?.addEventListener('abort', onAbort, { once: true });
|
for (const wait of waits)
|
||||||
attached.then(
|
wait.removeEventListener('abort', onAbort);
|
||||||
() => {
|
resolve();
|
||||||
signal?.removeEventListener('abort', onAbort);
|
};
|
||||||
resolve(data);
|
if (waits.some((wait) => wait.aborted)) return onAbort();
|
||||||
},
|
for (const wait of waits)
|
||||||
|
wait.addEventListener('abort', onAbort, { once: true });
|
||||||
|
binding.then(
|
||||||
|
() => onAbort(),
|
||||||
(error) => {
|
(error) => {
|
||||||
signal?.removeEventListener('abort', onAbort);
|
for (const wait of waits)
|
||||||
|
wait.removeEventListener('abort', onAbort);
|
||||||
reject(error);
|
reject(error);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
async detachScene() {
|
||||||
|
if (sessionGoes()) {
|
||||||
|
await dataManager.destroyAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await dataManager.detachScene();
|
||||||
|
presentation.suspend();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ defer },
|
||||||
|
);
|
||||||
|
const data = {
|
||||||
|
dataManager,
|
||||||
|
get catalog() {
|
||||||
|
return current;
|
||||||
|
},
|
||||||
|
presentation,
|
||||||
|
};
|
||||||
|
return attached?.then ? attached.then(() => data) : data;
|
||||||
}
|
}
|
||||||
@@ -59,11 +59,13 @@ test('the scene constructs the engine with the viewer and returns it beside the
|
|||||||
|
|
||||||
test('the debug handle exposes the engine beside the viewer', () => {
|
test('the debug handle exposes the engine beside the viewer', () => {
|
||||||
const tools = read('src/app/tools.js');
|
const tools = read('src/app/tools.js');
|
||||||
|
// The handle belongs to the application; its globe fields are the
|
||||||
|
// attached scene's.
|
||||||
|
assert.match(tools, /window\.__godsEyeView = debug;/);
|
||||||
assert.match(
|
assert.match(
|
||||||
tools,
|
tools,
|
||||||
/const \{ viewer, engine, tileset, mapStackController, operations \} = scene;/,
|
/Object\.assign\(debug, \{\n\s+viewer: next\.viewer \?\? null,\n\s+engine: next\.engine,/,
|
||||||
);
|
);
|
||||||
assert.match(tools, /window\.__godsEyeView = \{\n\s+viewer,\n\s+engine,/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the scene engine is the one engineFromViewer returns for its viewer until teardown', async () => {
|
test('the scene engine is the one engineFromViewer returns for its viewer until teardown', async () => {
|
||||||
|
|||||||
+18
-47
@@ -12,7 +12,6 @@ import {
|
|||||||
registerLayerRenderers,
|
registerLayerRenderers,
|
||||||
} from './layers/renderers.js';
|
} from './layers/renderers.js';
|
||||||
import {
|
import {
|
||||||
getRenderGovernorDiagnostics,
|
|
||||||
governorRequestRender,
|
governorRequestRender,
|
||||||
installRenderGovernor,
|
installRenderGovernor,
|
||||||
uninstallRenderGovernor,
|
uninstallRenderGovernor,
|
||||||
@@ -389,24 +388,25 @@ export async function createNavaraScene({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tools without a viewer (the experimental Navara engine): the startup
|
* The tools without a viewer (the experimental Navara engine), attached to
|
||||||
* chrome, the render governor on the engine, hidden-tab handling, voice on
|
* one Navara scene: the render governor on the engine, hidden-tab handling,
|
||||||
* the engine and the debug handle. The shell shows which features are
|
* the imagery box, the engine notice and voice on the engine. The shell
|
||||||
* unavailable.
|
* shows which features are unavailable. Returns its annotations (none) and
|
||||||
|
* voice.
|
||||||
*/
|
*/
|
||||||
export function createViewerlessTools({
|
export function attachViewerlessTools(
|
||||||
scene: { engine, mapStackController, operations },
|
{ engine },
|
||||||
controls: { styleManager },
|
{
|
||||||
data,
|
data,
|
||||||
loadingScreen,
|
operations,
|
||||||
placeSearch,
|
placeSearch,
|
||||||
voice = {},
|
voice = {},
|
||||||
startChrome,
|
|
||||||
sceneDirector,
|
sceneDirector,
|
||||||
layerSession = null,
|
layerSession = null,
|
||||||
signal,
|
styleManager,
|
||||||
defer,
|
},
|
||||||
}) {
|
{ signal, defer },
|
||||||
|
) {
|
||||||
const { dataManager } = data;
|
const { dataManager } = data;
|
||||||
void styleManager.initialRestorePromise?.then(() => {
|
void styleManager.initialRestorePromise?.then(() => {
|
||||||
if (!signal.aborted)
|
if (!signal.aborted)
|
||||||
@@ -441,18 +441,8 @@ export function createViewerlessTools({
|
|||||||
tool.destroy();
|
tool.destroy();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// No first-run welcome: its choices turn on data layers.
|
// Holds drive engine.render.setContinuous; requests go to the engine. A
|
||||||
if (startChrome)
|
// parked engine uninstalls it (createNavaraScene suspend).
|
||||||
defer(
|
|
||||||
startChrome({
|
|
||||||
loadingScreen,
|
|
||||||
styleManager,
|
|
||||||
dataManager,
|
|
||||||
signal,
|
|
||||||
initializeWelcome: null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
// Holds drive engine.render.setContinuous; requests go to the engine.
|
|
||||||
installRenderGovernor(engine);
|
installRenderGovernor(engine);
|
||||||
const syncVisibility = () => {
|
const syncVisibility = () => {
|
||||||
if (document.hidden && !isEmbeddedInline()) return;
|
if (document.hidden && !isEmbeddedInline()) return;
|
||||||
@@ -469,26 +459,8 @@ export function createViewerlessTools({
|
|||||||
detail: notice.detail,
|
detail: notice.detail,
|
||||||
persistent: true,
|
persistent: true,
|
||||||
});
|
});
|
||||||
window.__godsEyeView = {
|
// The notice is this engine's: it goes when the engine does.
|
||||||
viewer: null,
|
defer(() => styleManager._clearPersistentStatusNotice?.());
|
||||||
engine,
|
|
||||||
styleManager,
|
|
||||||
tileset: null,
|
|
||||||
dataManager,
|
|
||||||
sceneDirector,
|
|
||||||
mapStackController,
|
|
||||||
annotations: null,
|
|
||||||
weatherEffects: null,
|
|
||||||
cockpitCloudEffects: null,
|
|
||||||
voiceCommands: null,
|
|
||||||
getRenderGovernorDiagnostics,
|
|
||||||
surfaceServices: operations.surface,
|
|
||||||
requestRender: governorRequestRender,
|
|
||||||
};
|
|
||||||
const debug = window.__godsEyeView;
|
|
||||||
defer(() => {
|
|
||||||
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
|
||||||
});
|
|
||||||
// Voice on the engine (phase 3, Task 9): the W1 tools drive the engine;
|
// Voice on the engine (phase 3, Task 9): the W1 tools drive the engine;
|
||||||
// the Director uses the shared session; viewer-only tools declare availability.
|
// the Director uses the shared session; viewer-only tools declare availability.
|
||||||
const voiceCommands = attachSessionVoice({
|
const voiceCommands = attachSessionVoice({
|
||||||
@@ -507,6 +479,5 @@ export function createViewerlessTools({
|
|||||||
signal,
|
signal,
|
||||||
defer,
|
defer,
|
||||||
});
|
});
|
||||||
debug.voiceCommands = voiceCommands;
|
return { annotations: null, voiceCommands };
|
||||||
return { sceneDirector, annotations: null, voiceCommands };
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// Parts of the application bound to one globe engine (see sceneHost.js).
|
||||||
|
// A page that switches engines gives its phases a scene with a `sceneHost`:
|
||||||
|
// each part registers there and attaches to every engine the page shows.
|
||||||
|
// A host that never switches gives a plain scene: the part attaches to it
|
||||||
|
// once and detaches with the application.
|
||||||
|
|
||||||
|
/** The concrete scene a phase was built on (a switching scene's current one). */
|
||||||
|
export function currentSceneOf(scene) {
|
||||||
|
return typeof scene?.current === 'function' ? scene.current() : scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach `part` ({ name, attachScene(scene, { signal, defer }),
|
||||||
|
* detachScene(options) }) to `scene` and keep it attached for the
|
||||||
|
* application: through the scene host when the page switches engines,
|
||||||
|
* else once, released by the phase's `defer`. Resolves with the first
|
||||||
|
* attachment's result.
|
||||||
|
*/
|
||||||
|
export function attachScenePart(scene, part, { defer }) {
|
||||||
|
if (scene?.sceneHost) return scene.sceneHost.register(part);
|
||||||
|
const controller = new AbortController();
|
||||||
|
const cleanups = [];
|
||||||
|
defer(async () => {
|
||||||
|
controller.abort();
|
||||||
|
const errors = [];
|
||||||
|
while (cleanups.length) {
|
||||||
|
try {
|
||||||
|
await cleanups.pop()();
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await part.detachScene?.({ handoff: false });
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(error);
|
||||||
|
}
|
||||||
|
if (errors.length)
|
||||||
|
throw new AggregateError(errors, `${part.name} could not detach`);
|
||||||
|
});
|
||||||
|
return part.attachScene?.(scene, {
|
||||||
|
signal: controller.signal,
|
||||||
|
defer(cleanup) {
|
||||||
|
if (typeof cleanup !== 'function')
|
||||||
|
throw new TypeError('A scene part defers a function');
|
||||||
|
cleanups.push(cleanup);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach every part when the application is torn down, before the phase's
|
||||||
|
* own session cleanup runs (a no-op without a scene host, or once detached).
|
||||||
|
*/
|
||||||
|
export function detachPartsOnTeardown(scene, defer, options) {
|
||||||
|
const host = scene?.sceneHost;
|
||||||
|
if (!host) return;
|
||||||
|
defer(() => host.detach(typeof options === 'function' ? options() : options));
|
||||||
|
}
|
||||||
+181
-55
@@ -7,6 +7,11 @@ import { applicationPinia } from '../ui/vueRuntime.js';
|
|||||||
import { initImageryBoxTool } from '../ui/imageryBoxTool.js';
|
import { initImageryBoxTool } from '../ui/imageryBoxTool.js';
|
||||||
import { createRecentImageryPanel } from '../ui/recentImagery.js';
|
import { createRecentImageryPanel } from '../ui/recentImagery.js';
|
||||||
import { attachSessionVoice } from './sessionVoice.js';
|
import { attachSessionVoice } from './sessionVoice.js';
|
||||||
|
import {
|
||||||
|
attachScenePart,
|
||||||
|
currentSceneOf,
|
||||||
|
detachPartsOnTeardown,
|
||||||
|
} from './sceneParts.js';
|
||||||
import { installViews, isEmbeddedInline } from './embed.js';
|
import { installViews, isEmbeddedInline } from './embed.js';
|
||||||
import { installScopeMask, destroyScopeMask } from '../scopeMask.js';
|
import { installScopeMask, destroyScopeMask } from '../scopeMask.js';
|
||||||
import { installLayerReleaseTrim } from '../sceneCommandRelease.js';
|
import { installLayerReleaseTrim } from '../sceneCommandRelease.js';
|
||||||
@@ -74,12 +79,16 @@ function destroyAnnotations(annotations) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach scene tools, rendering listeners and the application debug handle.
|
* Attach scene tools, rendering listeners and the application debug handle.
|
||||||
* Without a viewer (the experimental Navara engine) the viewer-bound tools
|
* The Director, the annotation board, Draw, Import, voice and the debug
|
||||||
* (director, annotations, drawing, imagery box, scope mask, tracking,
|
* handle belong to the application; the tools bound to one globe (render
|
||||||
* embedded views) are not created; their handles are null. Voice runs on
|
* governor, release trim, scope mask, imagery box, tracking and visibility
|
||||||
* the engine there (src/app/navaraMode.js).
|
* listeners, embedded views, the debug handle's globe fields) attach to each
|
||||||
|
* engine the page shows (src/app/sceneParts.js). Without a viewer (the
|
||||||
|
* experimental Navara engine) the viewer-bound tools are not attached and
|
||||||
|
* their handles are null; voice runs on the engine there
|
||||||
|
* (src/app/navaraMode.js).
|
||||||
*/
|
*/
|
||||||
export function createApplicationTools({
|
export async function createApplicationTools({
|
||||||
scene,
|
scene,
|
||||||
controls,
|
controls,
|
||||||
data,
|
data,
|
||||||
@@ -93,26 +102,32 @@ export function createApplicationTools({
|
|||||||
signal,
|
signal,
|
||||||
defer,
|
defer,
|
||||||
}) {
|
}) {
|
||||||
const { viewer, engine, tileset, mapStackController, operations } = scene;
|
const { operations } = scene;
|
||||||
const { styleManager, weatherEffects, cockpitCloudEffects } = controls;
|
const first = currentSceneOf(scene);
|
||||||
|
const { styleManager } = controls;
|
||||||
const { dataManager } = data;
|
const { dataManager } = data;
|
||||||
const isMapStackAvailable = (id) =>
|
// The shell leaves its globe before the data layers detach (registered
|
||||||
|
// after them, so it detaches first): its share restoration and Context
|
||||||
|
// transactions end while the layers and the viewer still exist.
|
||||||
|
await attachScenePart(
|
||||||
|
scene,
|
||||||
|
{
|
||||||
|
name: 'shell-release',
|
||||||
|
detachScene: () => styleManager.detachScene?.(),
|
||||||
|
},
|
||||||
|
{ defer },
|
||||||
|
);
|
||||||
|
const isStackAvailable = (mapStackController) => (id) =>
|
||||||
mapStackController?.isStackAvailable(id) === true;
|
mapStackController?.isStackAvailable(id) === true;
|
||||||
const sceneDirector =
|
const sceneDirector =
|
||||||
layerSession?.director ??
|
layerSession?.director ??
|
||||||
new SceneDirector(viewer, styleManager, dataManager, {
|
new SceneDirector(first.viewer, styleManager, dataManager, {
|
||||||
engine,
|
engine: first.engine,
|
||||||
dataPacks: { ...sceneDataPacks, native: scene.packRenderer },
|
dataPacks: { ...sceneDataPacks, native: first.packRenderer },
|
||||||
isMapStackAvailable,
|
isMapStackAvailable: isStackAvailable(first.mapStackController),
|
||||||
});
|
});
|
||||||
if (layerSession?.director)
|
// The Director built on this globe is attached to it already.
|
||||||
sceneDirector.attachScene(
|
let directorScene = layerSession?.director ? null : first;
|
||||||
viewer,
|
|
||||||
engine,
|
|
||||||
styleManager,
|
|
||||||
scene.packRenderer,
|
|
||||||
isMapStackAvailable,
|
|
||||||
);
|
|
||||||
if (layerSession) layerSession.director = sceneDirector;
|
if (layerSession) layerSession.director = sceneDirector;
|
||||||
dataManager.layers
|
dataManager.layers
|
||||||
.get('bhote-koshi-2026')
|
.get('bhote-koshi-2026')
|
||||||
@@ -126,24 +141,156 @@ export function createApplicationTools({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
onSceneDirector?.(sceneDirector);
|
onSceneDirector?.(sceneDirector);
|
||||||
// Without a viewer (the Navara mode) the tools load with that mode's code.
|
await attachScenePart(
|
||||||
if (!viewer) {
|
|
||||||
return import('./navaraMode.js').then(({ createViewerlessTools }) =>
|
|
||||||
createViewerlessTools({
|
|
||||||
scene,
|
scene,
|
||||||
|
{
|
||||||
|
name: 'director',
|
||||||
|
attachScene(next) {
|
||||||
|
if (next === directorScene) return;
|
||||||
|
directorScene = null;
|
||||||
|
sceneDirector.attachScene(
|
||||||
|
next.viewer,
|
||||||
|
next.engine,
|
||||||
|
styleManager,
|
||||||
|
next.packRenderer,
|
||||||
|
isStackAvailable(next.mapStackController),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// The session keeps the Director; the page teardown destroys it.
|
||||||
|
detachScene: () => sceneDirector.suspendScene(),
|
||||||
|
},
|
||||||
|
{ defer },
|
||||||
|
);
|
||||||
|
|
||||||
|
// The application's debug handle; its globe fields follow the engine.
|
||||||
|
const debug = {
|
||||||
|
viewer: null,
|
||||||
|
engine: null,
|
||||||
|
styleManager,
|
||||||
|
tileset: null,
|
||||||
|
dataManager,
|
||||||
|
sceneDirector,
|
||||||
|
mapStackController: null,
|
||||||
|
annotations: null,
|
||||||
|
weatherEffects: controls.weatherEffects ?? null,
|
||||||
|
cockpitCloudEffects: null,
|
||||||
|
voiceCommands: null,
|
||||||
|
getRenderGovernorDiagnostics,
|
||||||
|
surfaceServices: operations.surface,
|
||||||
|
requestRender: governorRequestRender,
|
||||||
|
};
|
||||||
|
window.__godsEyeView = debug;
|
||||||
|
defer(() => {
|
||||||
|
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The attached engine's tools; `annotations` and the voice handle answer
|
||||||
|
// for the engine attached now.
|
||||||
|
const attached = { annotations: null, voiceCommands: null };
|
||||||
|
const context = {
|
||||||
controls,
|
controls,
|
||||||
data,
|
data,
|
||||||
loadingScreen,
|
operations,
|
||||||
placeSearch,
|
placeSearch,
|
||||||
voice,
|
voice,
|
||||||
sceneDirector,
|
sceneDirector,
|
||||||
startChrome,
|
|
||||||
layerSession,
|
layerSession,
|
||||||
|
styleManager,
|
||||||
|
dataManager,
|
||||||
|
};
|
||||||
|
await attachScenePart(
|
||||||
|
scene,
|
||||||
|
{
|
||||||
|
name: 'tools',
|
||||||
|
async attachScene(next, attachment) {
|
||||||
|
const tools = next.viewer
|
||||||
|
? attachViewerTools(next, context, attachment)
|
||||||
|
: await import('./navaraMode.js').then(({ attachViewerlessTools }) =>
|
||||||
|
attachViewerlessTools(next, context, attachment),
|
||||||
|
);
|
||||||
|
attached.annotations = tools.annotations;
|
||||||
|
attached.voiceCommands = tools.voiceCommands;
|
||||||
|
attachment.defer(() => {
|
||||||
|
attached.annotations = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ defer },
|
||||||
|
);
|
||||||
|
// The debug handle names the attached globe once every part is on it.
|
||||||
|
await attachScenePart(
|
||||||
|
scene,
|
||||||
|
{
|
||||||
|
name: 'debug-handle',
|
||||||
|
attachScene(next) {
|
||||||
|
Object.assign(debug, {
|
||||||
|
viewer: next.viewer ?? null,
|
||||||
|
engine: next.engine,
|
||||||
|
tileset: next.tileset ?? null,
|
||||||
|
mapStackController: next.mapStackController ?? null,
|
||||||
|
annotations: attached.annotations,
|
||||||
|
cockpitCloudEffects: controls.cockpitCloudEffects ?? null,
|
||||||
|
voiceCommands: attached.voiceCommands,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
detachScene() {
|
||||||
|
Object.assign(debug, {
|
||||||
|
viewer: null,
|
||||||
|
engine: null,
|
||||||
|
tileset: null,
|
||||||
|
mapStackController: null,
|
||||||
|
annotations: null,
|
||||||
|
cockpitCloudEffects: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ defer },
|
||||||
|
);
|
||||||
|
detachPartsOnTeardown(scene, defer, () => ({
|
||||||
|
handoff: Boolean(
|
||||||
|
layerSession?.handoffLayers && !layerSession.lifetime.signal.aborted,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
// The startup chrome belongs to the page: no first-run welcome on Navara
|
||||||
|
// (its choices turn on data layers).
|
||||||
|
if (startChrome)
|
||||||
|
defer(
|
||||||
|
startChrome({
|
||||||
|
loadingScreen,
|
||||||
|
styleManager,
|
||||||
|
dataManager,
|
||||||
signal,
|
signal,
|
||||||
defer,
|
...(first.viewer ? {} : { initializeWelcome: null }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
return {
|
||||||
|
sceneDirector,
|
||||||
|
get annotations() {
|
||||||
|
return attached.annotations;
|
||||||
|
},
|
||||||
|
get voiceCommands() {
|
||||||
|
return attached.voiceCommands;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The tools bound to a Cesium viewer. Returns its annotations and voice. */
|
||||||
|
function attachViewerTools(
|
||||||
|
scene,
|
||||||
|
{
|
||||||
|
controls,
|
||||||
|
data,
|
||||||
|
operations,
|
||||||
|
placeSearch,
|
||||||
|
voice,
|
||||||
|
sceneDirector,
|
||||||
|
layerSession,
|
||||||
|
styleManager,
|
||||||
|
dataManager,
|
||||||
|
},
|
||||||
|
{ signal, defer },
|
||||||
|
) {
|
||||||
|
const { viewer, engine, tileset, mapStackController } = scene;
|
||||||
// The annotation board belongs to the page session when there is one: an
|
// The annotation board belongs to the page session when there is one: an
|
||||||
// engine switch only takes its drawing off this viewer.
|
// engine switch only takes its drawing off this viewer.
|
||||||
const annotations = sessionAnnotations({
|
const annotations = sessionAnnotations({
|
||||||
@@ -194,8 +341,8 @@ export function createApplicationTools({
|
|||||||
});
|
});
|
||||||
defer(() => geoFileImport?.detachScene());
|
defer(() => geoFileImport?.detachScene());
|
||||||
// DATA ▸ Recent Imagery: the box tool claims the pointer like Draw and the
|
// DATA ▸ Recent Imagery: the box tool claims the pointer like Draw and the
|
||||||
// panel lives on the right rail, so both belong to the application
|
// panel lives on the right rail, so both belong to this globe. The tileset
|
||||||
// lifetime. The tileset lets the layer drape while the globe is hidden.
|
// lets the layer drape while the globe is hidden.
|
||||||
const recentImagery = dataManager.layers.get('recent-imagery')?.module;
|
const recentImagery = dataManager.layers.get('recent-imagery')?.module;
|
||||||
if (recentImagery) {
|
if (recentImagery) {
|
||||||
recentImagery.attachTileset(tileset);
|
recentImagery.attachTileset(tileset);
|
||||||
@@ -230,11 +377,10 @@ export function createApplicationTools({
|
|||||||
imageryBoxTool?.destroy();
|
imageryBoxTool?.destroy();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (startChrome)
|
|
||||||
defer(startChrome({ loadingScreen, styleManager, dataManager, signal }));
|
|
||||||
// Idle render governor: flips the scene into requestRenderMode whenever
|
// Idle render governor: flips the scene into requestRenderMode whenever
|
||||||
// nothing animates per frame. Installed AFTER every module above has had
|
// nothing animates per frame. Installed AFTER every module above has had
|
||||||
// its chance to register pre-install holds. (perf wave 2)
|
// its chance to register pre-install holds. (perf wave 2) A parked viewer
|
||||||
|
// uninstalls it (src/app/scene.js suspend).
|
||||||
installRenderGovernor(viewer);
|
installRenderGovernor(viewer);
|
||||||
// Cesium keeps a turned-off layer's draw commands (and so all its entities)
|
// Cesium keeps a turned-off layer's draw commands (and so all its entities)
|
||||||
// alive in reused per-pass arrays; trim them after each disable.
|
// alive in reused per-pass arrays; trim them after each disable.
|
||||||
@@ -269,7 +415,7 @@ export function createApplicationTools({
|
|||||||
// keeps drawing itself (see keepPanelRendering in embed.js).
|
// keeps drawing itself (see keepPanelRendering in embed.js).
|
||||||
const hidden = document.hidden && !isEmbeddedInline();
|
const hidden = document.hidden && !isEmbeddedInline();
|
||||||
viewer.useDefaultRenderLoop = !hidden;
|
viewer.useDefaultRenderLoop = !hidden;
|
||||||
cockpitCloudEffects?.setSuspended?.(hidden);
|
controls.cockpitCloudEffects?.setSuspended?.(hidden);
|
||||||
if (!hidden) {
|
if (!hidden) {
|
||||||
data.presentation.flushVisible();
|
data.presentation.flushVisible();
|
||||||
governorRequestRender('visibility-restore');
|
governorRequestRender('visibility-restore');
|
||||||
@@ -288,25 +434,6 @@ export function createApplicationTools({
|
|||||||
// loop burning behind a hidden tab. (perf wave 2 fix)
|
// loop burning behind a hidden tab. (perf wave 2 fix)
|
||||||
syncVisibilitySuspension();
|
syncVisibilitySuspension();
|
||||||
|
|
||||||
window.__godsEyeView = {
|
|
||||||
viewer,
|
|
||||||
engine,
|
|
||||||
styleManager,
|
|
||||||
tileset,
|
|
||||||
dataManager,
|
|
||||||
sceneDirector,
|
|
||||||
mapStackController,
|
|
||||||
annotations,
|
|
||||||
weatherEffects,
|
|
||||||
cockpitCloudEffects,
|
|
||||||
getRenderGovernorDiagnostics,
|
|
||||||
surfaceServices: operations.surface,
|
|
||||||
requestRender: governorRequestRender,
|
|
||||||
};
|
|
||||||
const debug = window.__godsEyeView;
|
|
||||||
defer(() => {
|
|
||||||
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
|
||||||
});
|
|
||||||
const voiceCommands = attachSessionVoice({
|
const voiceCommands = attachSessionVoice({
|
||||||
layerSession,
|
layerSession,
|
||||||
voice,
|
voice,
|
||||||
@@ -317,7 +444,6 @@ export function createApplicationTools({
|
|||||||
signal,
|
signal,
|
||||||
defer,
|
defer,
|
||||||
});
|
});
|
||||||
debug.voiceCommands = voiceCommands;
|
|
||||||
defer(
|
defer(
|
||||||
installViews({
|
installViews({
|
||||||
shell: styleManager,
|
shell: styleManager,
|
||||||
@@ -327,5 +453,5 @@ export function createApplicationTools({
|
|||||||
signal,
|
signal,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return { sceneDirector, annotations, voiceCommands };
|
return { annotations, voiceCommands };
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user