feat(app): construct the globe engine with the viewer

The scene builds the page's Cesium engine right after the viewer
(src/app/globeEngine.js), names the startup Google tileset to it once
loaded, returns it beside the viewer and destroys it before the viewer.
The debug handle exposes it as window.__godsEyeView.engine. Nothing
consumes it yet; render holds taken before the governor is installed
apply at install.

qa-globe-contract now runs on the app's engine (self-built only when the
app has none); a throwaway engine checks creation and destroy leave the
scene untouched.
This commit is contained in:
German Khokhlov committed 2026-10-15 18:02:11 +04:00
1 parent ab2368f3f3
commit a5c98c6b07
7 files changed
+141 -2

No files matched your search

+2
View File
@@ -736,6 +736,8 @@
"src/globe/cesium/createCesiumEngine.test.mjs", "src/globe/cesium/createCesiumEngine.test.mjs",
"src/globe/cesium/testing/fakeViewer.mjs", "src/globe/cesium/testing/fakeViewer.mjs",
"scripts/qa-globe-contract.mjs", "scripts/qa-globe-contract.mjs",
"src/app/globeEngine.js",
"src/app/globeEngine.test.mjs",
"src/sources/reference.js", "src/sources/reference.js",
"src/sources/reference.test.mjs", "src/sources/reference.test.mjs",
"src/search/nominatim.js", "src/search/nominatim.js",
+14
View File
@@ -1737,6 +1737,20 @@
"src/app/layers/traffic.js", "src/app/layers/traffic.js",
"src/app/operations.js", "src/app/operations.js",
"src/app/scene.js", "src/app/scene.js",
"src/app/globeEngine.js",
"src/globe/contract.js",
"src/globe/cesium/createCesiumEngine.js",
"src/globe/cesium/camera.js",
"src/globe/cesium/convert.js",
"src/globe/cesium/credits.js",
"src/globe/cesium/events.js",
"src/globe/cesium/frames.js",
"src/globe/cesium/input.js",
"src/globe/cesium/maps.js",
"src/globe/cesium/picker.js",
"src/globe/cesium/projector.js",
"src/globe/cesium/render.js",
"src/globe/cesium/surface.js",
"src/sources/sourceSlot.js", "src/sources/sourceSlot.js",
"src/app/sources.js", "src/app/sources.js",
"src/app/startupChrome.js", "src/app/startupChrome.js",
+26
View File
@@ -83,6 +83,32 @@ test('startup shares one promise and teardown keeps data alive for controls', as
await assert.rejects(app.start(), /destroyed/); await assert.rejects(app.start(), /destroyed/);
}); });
test('later components receive the scene engine beside its viewer', async () => {
const viewer = { name: 'viewer' };
const engine = { kind: 'cesium' };
const seen = [];
const receive =
(phase) =>
({ scene }) => {
seen.push([phase, scene.viewer, scene.engine]);
return { name: phase };
};
const { app } = fixture({
createScene: () => ({ viewer, engine }),
createControls: receive('Controls'),
createData: receive('Data'),
createTools: receive('Tools'),
});
const components = await app.start();
assert.equal(components.scene.engine, engine);
assert.deepEqual(seen, [
['Controls', viewer, engine],
['Data', viewer, engine],
['Tools', viewer, engine],
]);
await app.destroy();
});
for (const phase of phases) { for (const phase of phases) {
test(`failure during ${phase} releases partially acquired resources`, async () => { test(`failure during ${phase} releases partially acquired resources`, async () => {
const { app, events } = fixture({ const { app, events } = fixture({
+23
View File
@@ -0,0 +1,23 @@
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
/**
* Build the page's one globe engine on the application viewer and tie its
* teardown to the scene lifetime. Call after the viewer's own cleanup is
* deferred, so the engine is destroyed first. Render holds taken before the
* render governor is installed apply when it is (src/renderGovernor.js).
* @param {Cesium.Viewer} viewer
* @param {{
* tilesetRef: { current: object | null } | (() => object | null) | null,
* defer: (dispose: () => void) => void,
* createEngine?: typeof createCesiumEngine,
* }} options
* @returns {import('../globe/contract.js').GlobeEngine}
*/
export function createSceneEngine(
viewer,
{ tilesetRef, defer, createEngine = createCesiumEngine },
) {
const engine = createEngine(viewer, { tilesetRef });
defer(() => engine.destroy());
return engine;
}
+67
View File
@@ -0,0 +1,67 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createSceneEngine } from './globeEngine.js';
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
const ROOT = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
);
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
test('the scene engine is built on the viewer and destroyed by the scene lifetime', () => {
const viewer = { name: 'viewer' };
const tilesetRef = { current: null };
const cleanups = [];
const calls = [];
const engine = createSceneEngine(viewer, {
tilesetRef,
defer: (dispose) => cleanups.push(dispose),
createEngine(target, options) {
calls.push([target, options]);
return { kind: 'cesium', destroy: () => calls.push('destroy') };
},
});
assert.equal(engine.kind, 'cesium');
assert.equal(calls.length, 1);
assert.equal(calls[0][0], viewer);
assert.equal(calls[0][1].tilesetRef, tilesetRef);
assert.equal(cleanups.length, 1, 'engine teardown is deferred at once');
cleanups[0]();
assert.equal(calls.at(-1), 'destroy');
});
test('the scene engine defaults to the Cesium engine factory', () => {
assert.throws(
() => createSceneEngine({}, { tilesetRef: null, defer: () => {} }),
/createCesiumEngine needs a viewer/,
);
assert.equal(typeof createCesiumEngine, 'function');
});
test('the scene constructs the engine with the viewer and returns it beside the viewer', () => {
const scene = read('src/app/scene.js');
const viewerTeardown = scene.indexOf('uninstallRenderGovernor(viewer);');
const engineAt = scene.indexOf('createSceneEngine(viewer, {');
assert.ok(viewerTeardown >= 0 && engineAt >= 0);
// Cleanups run last-in first-out: the engine goes before the viewer.
assert.ok(engineAt > viewerTeardown, 'engine teardown must precede viewer');
assert.match(scene, /tilesetRef\.current = tileset;/);
assert.match(
scene,
/return \{ viewer, engine, tileset, mapStackController, operations \};/,
);
});
test('the debug handle exposes the engine beside the viewer', () => {
const tools = read('src/app/tools.js');
assert.match(
tools,
/const \{ viewer, engine, tileset, mapStackController, operations \} = scene;/,
);
assert.match(tools, /window\.__godsEyeView = \{\n\s+viewer,\n\s+engine,/);
});
+7 -1
View File
@@ -14,6 +14,7 @@ import {
governorRequestRender, governorRequestRender,
} from '../renderGovernor.js'; } from '../renderGovernor.js';
import { describeError } from './errors.js'; import { describeError } from './errors.js';
import { createSceneEngine } from './globeEngine.js';
/** Construct the application globe using the caller's local configuration. */ /** Construct the application globe using the caller's local configuration. */
export async function createApplicationScene({ export async function createApplicationScene({
@@ -56,6 +57,10 @@ export async function createApplicationScene({
uninstallRenderGovernor(viewer); uninstallRenderGovernor(viewer);
if (!viewer.isDestroyed()) viewer.destroy(); if (!viewer.isDestroyed()) viewer.destroy();
}); });
// The globe engine over this viewer; Google's startup tileset is named
// once it loads.
const tilesetRef = { current: null };
const engine = createSceneEngine(viewer, { tilesetRef, defer });
defer(installTrackpadPinchZoom(viewer)); defer(installTrackpadPinchZoom(viewer));
registerDataCredits(viewer, credits); registerDataCredits(viewer, credits);
configureCreditKeyboardAccess(document); configureCreditKeyboardAccess(document);
@@ -69,6 +74,7 @@ export async function createApplicationScene({
cesiumToken, cesiumToken,
}); });
const tileset = photoreal.tileset; const tileset = photoreal.tileset;
tilesetRef.current = tileset;
// A provider can finish after cancellation; retain ownership of its result. // A provider can finish after cancellation; retain ownership of its result.
defer(() => { defer(() => {
if (tileset && !tileset.isDestroyed()) { if (tileset && !tileset.isDestroyed()) {
@@ -121,5 +127,5 @@ export async function createApplicationScene({
}); });
signal.throwIfAborted(); signal.throwIfAborted();
return { viewer, tileset, mapStackController, operations }; return { viewer, engine, tileset, mapStackController, operations };
} }
+2 -1
View File
@@ -28,7 +28,7 @@ export function createApplicationTools({
signal, signal,
defer, defer,
}) { }) {
const { viewer, tileset, mapStackController, operations } = scene; const { viewer, engine, tileset, mapStackController, operations } = scene;
const { styleManager, weatherEffects, cockpitCloudEffects } = controls; const { styleManager, weatherEffects, cockpitCloudEffects } = controls;
const { dataManager } = data; const { dataManager } = data;
const sceneDirector = new SceneDirector(viewer, styleManager, dataManager, { const sceneDirector = new SceneDirector(viewer, styleManager, dataManager, {
@@ -144,6 +144,7 @@ export function createApplicationTools({
window.__godsEyeView = { window.__godsEyeView = {
viewer, viewer,
engine,
styleManager, styleManager,
tileset, tileset,
dataManager, dataManager,