mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 06:12:18 +00:00
Share the Cesium ports between the app and conformance renderers
The frame, draw, camera, query and capture ports move out of the conformance scene renderer into src/render/cesium/ports.js, unchanged. The app's transitional renderer, now src/render/cesium/appRenderer.js, builds the same ports over the full viewer; it still leaves out globe, whenSettled and setTime, which would misreport photoreal tiles and stop the app clock. Disposing the ports now settles a pending renderOnce and detaches the draw sets, so a model that loads after destroy is destroyed rather than added to a destroyed collection. A viewer whose ports fail to build is destroyed with its credits. Checked in a browser: the app boots with one credits element, and all eleven conformance scenes match their references exactly.
This commit is contained in:
1 parent
03f2e2a9b9
commit
a5143f4467
7 files changed
+770
-561
No files matched your search
@@ -0,0 +1,357 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import * as Cesium from 'cesium';
|
||||
import { CONTRACT_VERSION, checkRendererShape } from '../contract/index.js';
|
||||
import {
|
||||
ReadonlySet,
|
||||
cesiumRenderer,
|
||||
cesiumViewerOf,
|
||||
createCesiumRendererPlugin,
|
||||
} from './appRenderer.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,
|
||||
};
|
||||
}
|
||||
|
||||
/** A Cesium event: listeners added, and removed by the returned function. */
|
||||
function fakeEvent(listeners) {
|
||||
return {
|
||||
addEventListener(cb) {
|
||||
listeners.add(cb);
|
||||
return () => listeners.delete(cb);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fakeViewer(log, { failDestroy = false } = {}) {
|
||||
let destroyed = false;
|
||||
const listeners = new Set();
|
||||
return {
|
||||
listeners,
|
||||
scene: {
|
||||
camera: {},
|
||||
primitives: { add() {}, remove() {} },
|
||||
requestRender() {},
|
||||
preUpdate: fakeEvent(listeners),
|
||||
preRender: fakeEvent(listeners),
|
||||
postRender: fakeEvent(listeners),
|
||||
},
|
||||
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;
|
||||
let viewer;
|
||||
const plugin = createCesiumRendererPlugin({
|
||||
createViewer(options) {
|
||||
given = options;
|
||||
viewer = fakeViewer(log);
|
||||
return viewer;
|
||||
},
|
||||
});
|
||||
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);
|
||||
|
||||
assert.ok(viewer.listeners.size > 0, 'the ports follow the frame phases');
|
||||
assert.equal(typeof renderer.draw.icons, 'function');
|
||||
assert.ok(renderer.capabilities.has('models'), 'draw.models is declared');
|
||||
|
||||
renderer.resize();
|
||||
renderer.destroy();
|
||||
assert.equal(viewer.listeners.size, 0, 'and stop on 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('work pending at destroy settles and lets go of the scene', async (t) => {
|
||||
withDocument(t, fakeDocument());
|
||||
const viewers = [];
|
||||
const plugin = createCesiumRendererPlugin({
|
||||
createViewer: () => {
|
||||
const viewer = fakeViewer([]);
|
||||
viewers.push(viewer);
|
||||
return viewer;
|
||||
},
|
||||
});
|
||||
|
||||
const renderer = await plugin.create({ container: {} });
|
||||
const rendered = renderer.frame.renderOnce();
|
||||
renderer.destroy();
|
||||
await rendered;
|
||||
assert.equal(viewers[0].listeners.size, 0, 'no one-shot render stays');
|
||||
await renderer.frame.renderOnce();
|
||||
|
||||
// A model that finishes loading after destroy is destroyed, not added.
|
||||
const load = Promise.withResolvers();
|
||||
const saved = Cesium.Model.fromGltfAsync;
|
||||
Cesium.Model.fromGltfAsync = () => load.promise;
|
||||
t.after(() => {
|
||||
Cesium.Model.fromGltfAsync = saved;
|
||||
});
|
||||
const again = await plugin.create({ container: {} });
|
||||
again.draw.models({ id: 'm' }).set([
|
||||
{
|
||||
id: 'a',
|
||||
url: 'a.glb',
|
||||
pos: { lon: 0, lat: 0, height: 0 },
|
||||
orientation: { heading: 0, pitch: 0, roll: 0 },
|
||||
},
|
||||
]);
|
||||
again.destroy();
|
||||
let destroyed = false;
|
||||
load.resolve({
|
||||
destroy: () => (destroyed = true),
|
||||
readyEvent: fakeEvent(new Set()),
|
||||
errorEvent: fakeEvent(new Set()),
|
||||
});
|
||||
await load.promise;
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.ok(destroyed, 'the late model is destroyed');
|
||||
});
|
||||
|
||||
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 members 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('../../app/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.cancelFlight',
|
||||
'camera.controls',
|
||||
'camera.controls.coast',
|
||||
'camera.controls.coast.drag',
|
||||
'camera.controls.coast.zoom',
|
||||
'camera.controls.collision',
|
||||
'camera.controls.enableRotate',
|
||||
'camera.controls.enableTilt',
|
||||
'camera.controls.enableZoom',
|
||||
'camera.controls.enabled',
|
||||
'camera.controls.maxDistance',
|
||||
'camera.controls.minDistance',
|
||||
'camera.controls.pitchRange',
|
||||
'camera.controls.suspend',
|
||||
'camera.controls.zoomFactor',
|
||||
'camera.controls.zoomInputs',
|
||||
'camera.flyTo',
|
||||
'camera.flyToBounds',
|
||||
'camera.flying',
|
||||
'camera.follow',
|
||||
'camera.followFramed',
|
||||
'camera.followOffset',
|
||||
'camera.followOffset.get',
|
||||
'camera.followOffset.set',
|
||||
'camera.followed',
|
||||
'camera.look',
|
||||
'camera.lookAt',
|
||||
'camera.matrices',
|
||||
'camera.move',
|
||||
'camera.on',
|
||||
'camera.onCommand',
|
||||
'camera.release',
|
||||
'camera.rotateAround',
|
||||
'camera.setFov',
|
||||
'camera.viewRectangle',
|
||||
'camera.zoom',
|
||||
'draw.atlas',
|
||||
'draw.clampState',
|
||||
'draw.on',
|
||||
'frame.diagnostics',
|
||||
'frame.holdContinuous',
|
||||
'frame.mode',
|
||||
'frame.setTargetFps',
|
||||
'frame.setTime',
|
||||
'frame.suspend',
|
||||
'query.clampToSurface',
|
||||
'query.drillPick',
|
||||
'query.isOccluded',
|
||||
'query.pickGlobe',
|
||||
'query.pickPosition',
|
||||
'query.pickRay',
|
||||
'query.projectMany',
|
||||
'query.rayCast',
|
||||
'query.sampleHeight',
|
||||
'query.sampleHeightNow',
|
||||
'query.sampleHeightNowMany',
|
||||
'renderer.credits',
|
||||
'renderer.effects',
|
||||
'renderer.globe',
|
||||
'renderer.input',
|
||||
'renderer.on',
|
||||
'renderer.quality',
|
||||
'renderer.readPixels',
|
||||
'renderer.releaseUnused',
|
||||
'renderer.whenSettled',
|
||||
].map((member) => `${member} is missing`);
|
||||
Reference in new issue
Block a user