mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 05:12:23 +00:00
Create the application scene through a renderer plugin
createApplicationScene takes a renderer plugin, defaulting to Cesium, which builds today's viewer and credits exactly as before. Its renderer is transitional: it has only the members it really has, with read-only capabilities, and a test pins the contract members still to come. Code not yet ported reaches the viewer through cesiumViewerOf, never through a renderer member. A plugin whose renderer has no viewer is refused and destroyed, since the application still runs on the viewer. The result now includes the renderer. Not yet checked in a browser.
This commit is contained in:
1 parent
bcd24c458b
commit
8d7f82f110
5 files changed
+383
-16
No files matched your search
@@ -671,6 +671,7 @@
|
||||
"src/services/application.js",
|
||||
"src/services/requests.js",
|
||||
"src/app/scene.js",
|
||||
"src/app/renderer.test.mjs",
|
||||
"src/geo/geo.test.mjs",
|
||||
"src/geo/celestial.test.mjs",
|
||||
"src/geo/quaternion.test.mjs",
|
||||
|
||||
@@ -2421,6 +2421,8 @@
|
||||
"src/app/layers/traffic.js",
|
||||
"src/app/operations.js",
|
||||
"src/app/scene.js",
|
||||
"src/app/renderer.js",
|
||||
"src/render/contract/index.js",
|
||||
"src/sources/sourceSlot.js",
|
||||
"src/app/sources.js",
|
||||
"src/app/startupChrome.js",
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @module app/renderer
|
||||
* @description The application's default renderer plugin: today's Cesium
|
||||
* viewer, set up exactly as before, behind the contract's `RendererPlugin`
|
||||
* (`src/render/contract/renderer.d.ts`).
|
||||
*
|
||||
* The renderer it makes is transitional: it has only the members it really
|
||||
* has, and the contract's ports arrive one layer family at a time (phase 2
|
||||
* of the renderer plan); `checkRendererShape` lists what is still missing.
|
||||
* Until a family is ported, its code reaches the Cesium viewer through
|
||||
* `cesiumViewerOf`, never through a renderer member, so no engine type
|
||||
* crosses the contract and each call marks code still to port.
|
||||
*/
|
||||
import { CONTRACT_VERSION } from '../render/contract/index.js';
|
||||
import { createApplicationViewer } from './viewer.js';
|
||||
|
||||
/**
|
||||
* The members a transitional renderer has today.
|
||||
* @typedef {Pick<import('../render/contract/renderer').Renderer,
|
||||
* 'id' | 'contractVersion' | 'capabilities' | 'canvas' | 'resize' | 'destroy'>}
|
||||
* TransitionalRenderer
|
||||
*/
|
||||
|
||||
const viewers = new WeakMap();
|
||||
|
||||
/** A Set no one can change once built, so `capabilities` stays true. */
|
||||
export class ReadonlySet extends Set {
|
||||
#sealed;
|
||||
constructor(values = []) {
|
||||
// Filled here, not by Set's constructor: it would call the guarded add
|
||||
// before the private field exists.
|
||||
super();
|
||||
for (const value of values) super.add(value);
|
||||
this.#sealed = true;
|
||||
}
|
||||
add(value) {
|
||||
if (this.#sealed) throw new TypeError('capabilities are read-only');
|
||||
return super.add(value);
|
||||
}
|
||||
delete() {
|
||||
throw new TypeError('capabilities are read-only');
|
||||
}
|
||||
clear() {
|
||||
throw new TypeError('capabilities are read-only');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Cesium viewer behind a renderer this plugin made, or null for any
|
||||
* other renderer.
|
||||
* @param {object} renderer
|
||||
* @returns {object|null}
|
||||
*/
|
||||
export const cesiumViewerOf = (renderer) => viewers.get(renderer) ?? null;
|
||||
|
||||
/**
|
||||
* The Cesium plugin over a viewer factory; tests pass a fake one.
|
||||
* @param {{createViewer?: typeof createApplicationViewer}} [options]
|
||||
*/
|
||||
export function createCesiumRendererPlugin({
|
||||
createViewer = createApplicationViewer,
|
||||
} = {}) {
|
||||
return Object.freeze({
|
||||
id: 'cesium',
|
||||
/**
|
||||
* Credentials stay with the scene, which still loads the tiles; they
|
||||
* move into the renderer with the globe port.
|
||||
* @param {{container: HTMLElement, signal: AbortSignal}} options
|
||||
* @returns {Promise<TransitionalRenderer>}
|
||||
*/
|
||||
async create({ container, signal }) {
|
||||
signal?.throwIfAborted();
|
||||
// Provider attribution stays visible, including clean-view and recording.
|
||||
const creditContainer = document.createElement('div');
|
||||
creditContainer.id = 'cesium-credits';
|
||||
document.body.appendChild(creditContainer);
|
||||
let viewer;
|
||||
try {
|
||||
viewer = createViewer({ container, creditContainer });
|
||||
} catch (error) {
|
||||
creditContainer.remove();
|
||||
throw error;
|
||||
}
|
||||
const renderer = Object.freeze({
|
||||
id: 'cesium',
|
||||
contractVersion: CONTRACT_VERSION,
|
||||
capabilities: new ReadonlySet(),
|
||||
canvas: viewer.canvas,
|
||||
resize() {
|
||||
viewer.resize();
|
||||
},
|
||||
destroy() {
|
||||
try {
|
||||
if (!viewer.isDestroyed()) viewer.destroy();
|
||||
} finally {
|
||||
creditContainer.remove();
|
||||
}
|
||||
},
|
||||
});
|
||||
viewers.set(renderer, viewer);
|
||||
return renderer;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** The default plugin. */
|
||||
export const cesiumRenderer = createCesiumRendererPlugin();
|
||||
@@ -0,0 +1,235 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
CONTRACT_VERSION,
|
||||
checkRendererShape,
|
||||
} from '../render/contract/index.js';
|
||||
import {
|
||||
ReadonlySet,
|
||||
cesiumRenderer,
|
||||
cesiumViewerOf,
|
||||
createCesiumRendererPlugin,
|
||||
} from './renderer.js';
|
||||
|
||||
/** A document with just a body, recording what is attached. */
|
||||
function fakeDocument() {
|
||||
const children = [];
|
||||
const make = () => {
|
||||
const el = {
|
||||
id: '',
|
||||
remove() {
|
||||
const i = children.indexOf(el);
|
||||
if (i >= 0) children.splice(i, 1);
|
||||
},
|
||||
};
|
||||
return el;
|
||||
};
|
||||
return {
|
||||
children,
|
||||
createElement: make,
|
||||
body: { appendChild: (el) => children.push(el) },
|
||||
getElementById: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
function fakeViewer(log, { failDestroy = false } = {}) {
|
||||
let destroyed = false;
|
||||
return {
|
||||
canvas: { tag: 'canvas' },
|
||||
resize: () => log.push('resize'),
|
||||
isDestroyed: () => destroyed,
|
||||
destroy() {
|
||||
log.push('destroy');
|
||||
if (failDestroy) throw new Error('destroy failed');
|
||||
destroyed = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function withDocument(t, doc) {
|
||||
const saved = globalThis.document;
|
||||
globalThis.document = doc;
|
||||
t.after(() => {
|
||||
globalThis.document = saved;
|
||||
});
|
||||
}
|
||||
|
||||
test('the default plugin is the Cesium renderer', () => {
|
||||
assert.equal(cesiumRenderer.id, 'cesium');
|
||||
assert.ok(Object.isFrozen(cesiumRenderer));
|
||||
});
|
||||
|
||||
test('the plugin wraps the viewer it is given, behind the accessor only', async (t) => {
|
||||
const doc = fakeDocument();
|
||||
withDocument(t, doc);
|
||||
const log = [];
|
||||
let given;
|
||||
const plugin = createCesiumRendererPlugin({
|
||||
createViewer(options) {
|
||||
given = options;
|
||||
return fakeViewer(log);
|
||||
},
|
||||
});
|
||||
const container = { tag: 'container' };
|
||||
const renderer = await plugin.create({
|
||||
container,
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
assert.equal(given.container, container);
|
||||
assert.equal(given.creditContainer.id, 'cesium-credits');
|
||||
assert.deepEqual(
|
||||
doc.children,
|
||||
[given.creditContainer],
|
||||
'credits on the page',
|
||||
);
|
||||
|
||||
assert.equal(renderer.id, 'cesium');
|
||||
assert.equal(renderer.contractVersion, CONTRACT_VERSION);
|
||||
assert.equal(renderer.canvas.tag, 'canvas');
|
||||
assert.ok(Object.isFrozen(renderer));
|
||||
assert.ok(renderer.capabilities instanceof Set);
|
||||
assert.throws(() => renderer.capabilities.add('models'), TypeError);
|
||||
assert.throws(() => renderer.capabilities.delete('models'), TypeError);
|
||||
assert.throws(() => renderer.capabilities.clear(), TypeError);
|
||||
assert.ok(!('viewer' in renderer), 'no engine object is a member');
|
||||
assert.equal(cesiumViewerOf(renderer).canvas, renderer.canvas);
|
||||
|
||||
renderer.resize();
|
||||
renderer.destroy();
|
||||
assert.deepEqual(log, ['resize', 'destroy']);
|
||||
assert.deepEqual(doc.children, [], 'credits leave with the renderer');
|
||||
renderer.destroy();
|
||||
assert.deepEqual(log, ['resize', 'destroy'], 'destroying twice is safe');
|
||||
});
|
||||
|
||||
test('the credits leave even when the viewer fails to destroy', async (t) => {
|
||||
const doc = fakeDocument();
|
||||
withDocument(t, doc);
|
||||
const plugin = createCesiumRendererPlugin({
|
||||
createViewer: () => fakeViewer([], { failDestroy: true }),
|
||||
});
|
||||
const renderer = await plugin.create({
|
||||
container: {},
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
assert.throws(() => renderer.destroy(), /destroy failed/);
|
||||
assert.deepEqual(doc.children, []);
|
||||
});
|
||||
|
||||
test('a viewer that fails to build leaves no credits behind', async (t) => {
|
||||
const doc = fakeDocument();
|
||||
withDocument(t, doc);
|
||||
const plugin = createCesiumRendererPlugin({
|
||||
createViewer() {
|
||||
throw new Error('no WebGL');
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
plugin.create({ container: {}, signal: new AbortController().signal }),
|
||||
/no WebGL/,
|
||||
);
|
||||
assert.deepEqual(doc.children, []);
|
||||
});
|
||||
|
||||
test('an aborted creation builds nothing', async (t) => {
|
||||
const doc = fakeDocument();
|
||||
withDocument(t, doc);
|
||||
let built = false;
|
||||
const plugin = createCesiumRendererPlugin({
|
||||
createViewer: () => {
|
||||
built = true;
|
||||
return fakeViewer([]);
|
||||
},
|
||||
});
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
await assert.rejects(
|
||||
plugin.create({ container: {}, signal: controller.signal }),
|
||||
{ name: 'AbortError' },
|
||||
);
|
||||
assert.equal(built, false);
|
||||
assert.deepEqual(doc.children, []);
|
||||
});
|
||||
|
||||
test('the transitional renderer lacks exactly the ports still to come', async (t) => {
|
||||
withDocument(t, fakeDocument());
|
||||
const renderer = await createCesiumRendererPlugin({
|
||||
createViewer: () => fakeViewer([]),
|
||||
}).create({ container: {}, signal: new AbortController().signal });
|
||||
// Each phase 2 port shrinks this list; nothing may be faked to shorten it.
|
||||
assert.deepEqual(checkRendererShape(renderer).sort(), EXPECTED_MISSING);
|
||||
});
|
||||
|
||||
test('a read-only set keeps the values it was built with', () => {
|
||||
const set = new ReadonlySet(['models', 'terrain']);
|
||||
assert.deepEqual([...set], ['models', 'terrain']);
|
||||
assert.ok(set.has('terrain'));
|
||||
assert.throws(() => set.add('x'), TypeError);
|
||||
assert.equal(set.size, 2);
|
||||
});
|
||||
|
||||
test('only renderers this plugin made reach a Cesium viewer', () => {
|
||||
assert.equal(cesiumViewerOf({ id: 'cesium' }), null);
|
||||
assert.equal(cesiumViewerOf({}), null);
|
||||
});
|
||||
|
||||
test('the application refuses a renderer it cannot run on, and destroys it', async (t) => {
|
||||
const { createApplicationScene } = await import('./scene.js');
|
||||
const { createApplicationRequestServices } =
|
||||
await import('../services/requests.js');
|
||||
const destroyed = [];
|
||||
const deferred = [];
|
||||
const plugin = {
|
||||
id: 'other',
|
||||
async create(options) {
|
||||
assert.ok(options.signal, 'the scene passes its signal');
|
||||
return {
|
||||
id: 'other',
|
||||
destroy: () => {
|
||||
destroyed.push('other');
|
||||
throw new Error('and it fails to go');
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const saved = { document: globalThis.document, window: globalThis.window };
|
||||
globalThis.document = fakeDocument();
|
||||
globalThis.window = new EventTarget();
|
||||
const warn = console.warn;
|
||||
console.warn = () => {};
|
||||
t.after(() => {
|
||||
for (const fn of deferred.reverse()) fn();
|
||||
console.warn = warn;
|
||||
globalThis.document = saved.document;
|
||||
globalThis.window = saved.window;
|
||||
});
|
||||
await assert.rejects(
|
||||
createApplicationScene({
|
||||
renderer: plugin,
|
||||
requestServices: createApplicationRequestServices(),
|
||||
loaderStatus: { textContent: '' },
|
||||
signal: new AbortController().signal,
|
||||
defer: (fn) => deferred.push(fn),
|
||||
}),
|
||||
/other renderer cannot run the application yet/,
|
||||
'the refusal is not hidden by the failed destroy',
|
||||
);
|
||||
assert.deepEqual(destroyed, ['other']);
|
||||
});
|
||||
|
||||
const EXPECTED_MISSING = [
|
||||
'camera',
|
||||
'capture',
|
||||
'credits',
|
||||
'draw',
|
||||
'effects',
|
||||
'frame',
|
||||
'globe',
|
||||
'input',
|
||||
'on',
|
||||
'quality',
|
||||
'query',
|
||||
'readPixels',
|
||||
'releaseUnused',
|
||||
'whenSettled',
|
||||
].map((member) => `renderer.${member} is missing`);
|
||||
+38
-16
@@ -1,9 +1,7 @@
|
||||
import { createApplicationOperations } from './operations.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
createApplicationViewer,
|
||||
installTrackpadPinchZoom,
|
||||
} from '../app/viewer.js';
|
||||
import { installTrackpadPinchZoom } from '../app/viewer.js';
|
||||
import { cesiumRenderer, cesiumViewerOf } from './renderer.js';
|
||||
import { registerDataCredits } from '../data/dataCredits.js';
|
||||
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
|
||||
import { MapStackController } from '../mapStackController.js';
|
||||
@@ -17,8 +15,14 @@ import {
|
||||
import { describeError } from './errors.js';
|
||||
import { onLocaleChange, t } from '../i18n/index.js';
|
||||
|
||||
/** Construct the application globe using the caller's local configuration. */
|
||||
/**
|
||||
* Construct the application globe using the caller's local configuration.
|
||||
* `renderer` is the `RendererPlugin` that draws it (default: Cesium). The
|
||||
* application still runs on the Cesium viewer until its layers are ported,
|
||||
* so a plugin whose renderer has none is refused.
|
||||
*/
|
||||
export async function createApplicationScene({
|
||||
renderer: rendererPlugin = cesiumRenderer,
|
||||
requestServices,
|
||||
googleApiKey,
|
||||
googleTokens = null,
|
||||
@@ -45,18 +49,29 @@ export async function createApplicationScene({
|
||||
});
|
||||
}
|
||||
loaderStatus.textContent = t('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,
|
||||
// The scene still loads the tiles, so their credentials stay here.
|
||||
const renderer = await rendererPlugin.create({
|
||||
container: document.getElementById('cesiumContainer'),
|
||||
signal,
|
||||
});
|
||||
const viewer = cesiumViewerOf(renderer);
|
||||
if (!viewer) {
|
||||
const refusal = new Error(
|
||||
`The ${rendererPlugin.id} renderer cannot run the application yet`,
|
||||
);
|
||||
try {
|
||||
renderer.destroy();
|
||||
} catch (error) {
|
||||
console.warn('[Init] Refused renderer failed to destroy:', error);
|
||||
}
|
||||
throw refusal;
|
||||
}
|
||||
defer(() => {
|
||||
uninstallRenderGovernor(viewer);
|
||||
if (!viewer.isDestroyed()) viewer.destroy();
|
||||
try {
|
||||
uninstallRenderGovernor(viewer);
|
||||
} finally {
|
||||
renderer.destroy();
|
||||
}
|
||||
});
|
||||
// Before any layer binds a click: every layer and tool shares this one.
|
||||
// Deferred after the viewer, so it is released before the viewer goes.
|
||||
@@ -133,5 +148,12 @@ export async function createApplicationScene({
|
||||
});
|
||||
|
||||
signal.throwIfAborted();
|
||||
return { viewer, tileset, mapStackController, operations, input };
|
||||
return {
|
||||
renderer,
|
||||
viewer,
|
||||
tileset,
|
||||
mapStackController,
|
||||
operations,
|
||||
input,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user