diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 79f49b275..f40cf571f 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -2661,6 +2661,7 @@ "src/app/surfaceServices.js", "src/app/tools.js", "src/app/sessionVoice.js", + "src/app/sceneParts.js", "src/app/viewer.js", "src/sky/starrySky.js", "src/sky/stars.js", diff --git a/src/app/controls.js b/src/app/controls.js index 647a0727b..fe18c9875 100644 --- a/src/app/controls.js +++ b/src/app/controls.js @@ -3,15 +3,20 @@ import { StyleManager } from '../ui/composition.js'; import { flyToAustin } from '../camera.js'; import { initCockpitCloudEffects } from '../cockpitCloudEffects.js'; import { t } from '../i18n/index.js'; +import { attachScenePart, currentSceneOf } from './sceneParts.js'; /** * Construct the existing controls and camera presentation. `initialShareCode` * opens a share code (the `` of `/share/`) at start-up, for a host * whose address does not carry one. `localePicker: false` removes the language * 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({ - scene: { viewer, engine, mapStackController, operations }, + scene, loaderStatus, Controls = StyleManager, services, @@ -23,14 +28,18 @@ export function createApplicationControls({ engineSwitch = false, layerSession, }) { + const { viewer, engine, mapStackController, operations } = scene; + const sharedServices = { + ...services, + ...operations.surface.controlServices, + searchAndFlyTo: operations.searchAndFlyTo, + fetchRegionalBrief: (...args) => + operations.requests.regional.getBrief(...args), + }; // Initialize the style manager (post-processing, HUD, locations, share links) const styleManager = new Controls(viewer, { services: { - ...services, - ...operations.surface.controlServices, - searchAndFlyTo: operations.searchAndFlyTo, - fetchRegionalBrief: (...args) => - operations.requests.regional.getBrief(...args), + ...sharedServices, // The catalog's layers; the ones this engine cannot show (Navara) // are empty stand-ins. ...catalogControlServices(catalog), @@ -49,16 +58,54 @@ export function createApplicationControls({ defer(() => styleManager.orbitController.stop()); defer(() => styleManager.hud.destroy()); 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 // 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 = viewer - ? initCockpitCloudEffects(viewer, { - weatherService: operations.requests.weather, - }) - : null; - defer(() => cockpitCloudEffects?.destroy()); + let cockpitCloudEffects = null; + attachScenePart( + scene, + { + name: 'cockpit-clouds', + attachScene(next, { defer: release }) { + if (!next.viewer) return; + const effects = initCockpitCloudEffects(next.viewer, { + weatherService: operations.requests.weather, + }); + cockpitCloudEffects = effects; + release(() => { + if (cockpitCloudEffects === effects) cockpitCloudEffects = null; + effects?.destroy(); + }); + }, + }, + { defer }, + ); // If no share link state, do default fly-to Austin if (!styleManager.hasShareState) { @@ -68,5 +115,11 @@ export function createApplicationControls({ loaderStatus.textContent = t('Restoring shared view...'); } - return { styleManager, weatherEffects, cockpitCloudEffects }; + return { + styleManager, + weatherEffects, + get cockpitCloudEffects() { + return cockpitCloudEffects; + }, + }; } diff --git a/src/app/controls.test.mjs b/src/app/controls.test.mjs index 67f0ddb60..24a6b5a8e 100644 --- a/src/app/controls.test.mjs +++ b/src/app/controls.test.mjs @@ -13,9 +13,14 @@ test('the controls pass the scene engine to the shell', () => { const controls = read('./controls.js'); assert.match( controls, - /scene: \{ viewer, engine, mapStackController, operations \}/, + /const \{ viewer, engine, mapStackController, operations \} = scene;/, ); 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', () => { diff --git a/src/app/data.js b/src/app/data.js index 24fd391cc..32b80b891 100644 --- a/src/app/data.js +++ b/src/app/data.js @@ -2,9 +2,17 @@ import { LayerLifecycle } from '../data/lifecycle.js'; import { LayerPresentation } from './layerPresentation.js'; import { createCyberSonarScene } from '../cyberSonarScene.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({ - scene: { viewer, engine, mapStackController }, + scene, controls: { styleManager }, catalog, allowQaRegistration, @@ -13,12 +21,13 @@ export function createApplicationData({ defer, signal = null, }) { + const first = currentSceneOf(scene); // Initialize data layer manager // The modules get { signal, engine } on every call (viewer null on Navara). const dataManager = layerSession?.manager ?? - new LayerLifecycle(viewer, { - engine, + new LayerLifecycle(first.viewer, { + engine: first.engine, allowQaRegistration, // A panel's host may report it hidden while it is on screen; its layers // keep polling (see keepPanelRendering in embed.js). @@ -26,11 +35,10 @@ export function createApplicationData({ getSourceAvailability: catalog?.getSourceAvailability, }); if (layerSession) layerSession.manager = dataManager; + const sessionGoes = () => + !layerSession || layerSession.lifetime.signal.aborted; defer(async () => { - if (layerSession && !layerSession.lifetime.signal.aborted) { - await dataManager.detachScene(); - return; - } + if (!sessionGoes()) return; await dataManager.destroyAll(); if (dataManager.layers.size) throw new Error( @@ -44,19 +52,11 @@ export function createApplicationData({ }); if (layerSession) layerSession.presentation = presentation; defer(() => { - if (layerSession && !layerSession.lifetime.signal.aborted) - presentation.suspend(); - else presentation.destroy(); + if (sessionGoes()) presentation.destroy(); }); onData?.(dataManager); if (!catalog?.layers || !catalog?.metadata) 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) { window.__gevQaRegisterLayer = (targetManager, layerModule) => { @@ -78,31 +78,76 @@ export function createApplicationData({ delete window.__gevQaUnregisterLayer; }); } - presentation.mount( - document.getElementById('data-toggles'), - catalog.weatherClock, - ); - styleManager.attachDataManager(dataManager); - // The sonar scene draws on the viewer; the Navara engine has none. - if (viewer) defer(createCyberSonarScene(viewer, dataManager)); - const data = { dataManager, catalog, presentation }; - if (!attached) return data; - // A teardown during startup must not wait for slow layer attachments: it - // ends this wait, and the data cleanup above releases the scene. - return new Promise((resolve, reject) => { - const onAbort = () => resolve(data); - if (signal?.aborted) onAbort(); - else signal?.addEventListener('abort', onAbort, { once: true }); - attached.then( - () => { - signal?.removeEventListener('abort', onAbort); - resolve(data); + // 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( + document.getElementById('data-toggles'), + nextCatalog.weatherClock, + ); + styleManager.attachDataManager(dataManager); + // The sonar scene draws on the viewer; the Navara engine has none. + if (next.viewer) + release(createCyberSonarScene(next.viewer, dataManager)); + if (!binding) return; + // A teardown or switch during the attachment must not wait for slow + // layer attachments: it ends this wait, and the detach releases the + // scene. + return new Promise((resolve, reject) => { + const waits = [signal, attachment].filter(Boolean); + const onAbort = () => { + for (const wait of waits) + wait.removeEventListener('abort', onAbort); + resolve(); + }; + if (waits.some((wait) => wait.aborted)) return onAbort(); + for (const wait of waits) + wait.addEventListener('abort', onAbort, { once: true }); + binding.then( + () => onAbort(), + (error) => { + for (const wait of waits) + wait.removeEventListener('abort', onAbort); + reject(error); + }, + ); + }); }, - (error) => { - signal?.removeEventListener('abort', onAbort); - 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; } diff --git a/src/app/globeEngine.test.mjs b/src/app/globeEngine.test.mjs index 6793672ff..72e05a2d9 100644 --- a/src/app/globeEngine.test.mjs +++ b/src/app/globeEngine.test.mjs @@ -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', () => { 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( 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 () => { diff --git a/src/app/navaraMode.js b/src/app/navaraMode.js index 6e0a406bb..123f854f1 100644 --- a/src/app/navaraMode.js +++ b/src/app/navaraMode.js @@ -12,7 +12,6 @@ import { registerLayerRenderers, } from './layers/renderers.js'; import { - getRenderGovernorDiagnostics, governorRequestRender, installRenderGovernor, uninstallRenderGovernor, @@ -389,24 +388,25 @@ export async function createNavaraScene({ } /** - * The tools without a viewer (the experimental Navara engine): the startup - * chrome, the render governor on the engine, hidden-tab handling, voice on - * the engine and the debug handle. The shell shows which features are - * unavailable. + * The tools without a viewer (the experimental Navara engine), attached to + * one Navara scene: the render governor on the engine, hidden-tab handling, + * the imagery box, the engine notice and voice on the engine. The shell + * shows which features are unavailable. Returns its annotations (none) and + * voice. */ -export function createViewerlessTools({ - scene: { engine, mapStackController, operations }, - controls: { styleManager }, - data, - loadingScreen, - placeSearch, - voice = {}, - startChrome, - sceneDirector, - layerSession = null, - signal, - defer, -}) { +export function attachViewerlessTools( + { engine }, + { + data, + operations, + placeSearch, + voice = {}, + sceneDirector, + layerSession = null, + styleManager, + }, + { signal, defer }, +) { const { dataManager } = data; void styleManager.initialRestorePromise?.then(() => { if (!signal.aborted) @@ -441,18 +441,8 @@ export function createViewerlessTools({ tool.destroy(); }); } - // No first-run welcome: its choices turn on data layers. - if (startChrome) - defer( - startChrome({ - loadingScreen, - styleManager, - dataManager, - signal, - initializeWelcome: null, - }), - ); - // Holds drive engine.render.setContinuous; requests go to the engine. + // Holds drive engine.render.setContinuous; requests go to the engine. A + // parked engine uninstalls it (createNavaraScene suspend). installRenderGovernor(engine); const syncVisibility = () => { if (document.hidden && !isEmbeddedInline()) return; @@ -469,26 +459,8 @@ export function createViewerlessTools({ detail: notice.detail, persistent: true, }); - window.__godsEyeView = { - viewer: null, - 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; - }); + // The notice is this engine's: it goes when the engine does. + defer(() => styleManager._clearPersistentStatusNotice?.()); // 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. const voiceCommands = attachSessionVoice({ @@ -507,6 +479,5 @@ export function createViewerlessTools({ signal, defer, }); - debug.voiceCommands = voiceCommands; - return { sceneDirector, annotations: null, voiceCommands }; + return { annotations: null, voiceCommands }; } diff --git a/src/app/sceneParts.js b/src/app/sceneParts.js new file mode 100644 index 000000000..ad2fe7d8c --- /dev/null +++ b/src/app/sceneParts.js @@ -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)); +} diff --git a/src/app/tools.js b/src/app/tools.js index cb7eff417..f3e5b9f24 100644 --- a/src/app/tools.js +++ b/src/app/tools.js @@ -7,6 +7,11 @@ import { applicationPinia } from '../ui/vueRuntime.js'; import { initImageryBoxTool } from '../ui/imageryBoxTool.js'; import { createRecentImageryPanel } from '../ui/recentImagery.js'; import { attachSessionVoice } from './sessionVoice.js'; +import { + attachScenePart, + currentSceneOf, + detachPartsOnTeardown, +} from './sceneParts.js'; import { installViews, isEmbeddedInline } from './embed.js'; import { installScopeMask, destroyScopeMask } from '../scopeMask.js'; import { installLayerReleaseTrim } from '../sceneCommandRelease.js'; @@ -74,12 +79,16 @@ function destroyAnnotations(annotations) { /** * Attach scene tools, rendering listeners and the application debug handle. - * Without a viewer (the experimental Navara engine) the viewer-bound tools - * (director, annotations, drawing, imagery box, scope mask, tracking, - * embedded views) are not created; their handles are null. Voice runs on - * the engine there (src/app/navaraMode.js). + * The Director, the annotation board, Draw, Import, voice and the debug + * handle belong to the application; the tools bound to one globe (render + * governor, release trim, scope mask, imagery box, tracking and visibility + * 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, controls, data, @@ -93,26 +102,32 @@ export function createApplicationTools({ signal, defer, }) { - const { viewer, engine, tileset, mapStackController, operations } = scene; - const { styleManager, weatherEffects, cockpitCloudEffects } = controls; + const { operations } = scene; + const first = currentSceneOf(scene); + const { styleManager } = controls; 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; const sceneDirector = layerSession?.director ?? - new SceneDirector(viewer, styleManager, dataManager, { - engine, - dataPacks: { ...sceneDataPacks, native: scene.packRenderer }, - isMapStackAvailable, + new SceneDirector(first.viewer, styleManager, dataManager, { + engine: first.engine, + dataPacks: { ...sceneDataPacks, native: first.packRenderer }, + isMapStackAvailable: isStackAvailable(first.mapStackController), }); - if (layerSession?.director) - sceneDirector.attachScene( - viewer, - engine, - styleManager, - scene.packRenderer, - isMapStackAvailable, - ); + // The Director built on this globe is attached to it already. + let directorScene = layerSession?.director ? null : first; if (layerSession) layerSession.director = sceneDirector; dataManager.layers .get('bhote-koshi-2026') @@ -126,24 +141,156 @@ export function createApplicationTools({ } }); onSceneDirector?.(sceneDirector); - // Without a viewer (the Navara mode) the tools load with that mode's code. - if (!viewer) { - return import('./navaraMode.js').then(({ createViewerlessTools }) => - createViewerlessTools({ - scene, - controls, - data, + await attachScenePart( + 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, + data, + operations, + placeSearch, + voice, + sceneDirector, + 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, - placeSearch, - voice, - sceneDirector, - startChrome, - layerSession, + styleManager, + dataManager, 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 // engine switch only takes its drawing off this viewer. const annotations = sessionAnnotations({ @@ -194,8 +341,8 @@ export function createApplicationTools({ }); defer(() => geoFileImport?.detachScene()); // 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 - // lifetime. The tileset lets the layer drape while the globe is hidden. + // panel lives on the right rail, so both belong to this globe. The tileset + // lets the layer drape while the globe is hidden. const recentImagery = dataManager.layers.get('recent-imagery')?.module; if (recentImagery) { recentImagery.attachTileset(tileset); @@ -230,11 +377,10 @@ export function createApplicationTools({ imageryBoxTool?.destroy(); }); } - if (startChrome) - defer(startChrome({ 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) + // its chance to register pre-install holds. (perf wave 2) A parked viewer + // uninstalls it (src/app/scene.js suspend). installRenderGovernor(viewer); // 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. @@ -269,7 +415,7 @@ export function createApplicationTools({ // keeps drawing itself (see keepPanelRendering in embed.js). const hidden = document.hidden && !isEmbeddedInline(); viewer.useDefaultRenderLoop = !hidden; - cockpitCloudEffects?.setSuspended?.(hidden); + controls.cockpitCloudEffects?.setSuspended?.(hidden); if (!hidden) { data.presentation.flushVisible(); governorRequestRender('visibility-restore'); @@ -288,25 +434,6 @@ export function createApplicationTools({ // loop burning behind a hidden tab. (perf wave 2 fix) 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({ layerSession, voice, @@ -317,7 +444,6 @@ export function createApplicationTools({ signal, defer, }); - debug.voiceCommands = voiceCommands; defer( installViews({ shell: styleManager, @@ -327,5 +453,5 @@ export function createApplicationTools({ signal, }), ); - return { sceneDirector, annotations, voiceCommands }; + return { annotations, voiceCommands }; }