mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Give the Cesium renderer the contract's listener rules and error channel
Frame-phase and draw-set ready listeners now run in subscription order,
skip one removed during delivery, defer one added during it, and hand a
throw to the renderer's error listeners (or reportError) instead of into
Cesium's render loop. The renderer gains on('error' | 'contextLost' |
'contextRestored'); Cesium cannot restore a lost context, so it never sends
contextRestored. Dispose clears every list. renderer.listeners joins the
Cesium checks.
This commit is contained in:
1 parent
3e48e453d0
commit
468590485b
6 files changed
+144
-19
No files matched your search
@@ -24,7 +24,7 @@ import { cesiumCapabilities, createCesiumPorts } from './ports.js';
|
|||||||
* The members a transitional renderer has today.
|
* The members a transitional renderer has today.
|
||||||
* @typedef {Pick<import('../contract/renderer').Renderer,
|
* @typedef {Pick<import('../contract/renderer').Renderer,
|
||||||
* 'id' | 'contractVersion' | 'capabilities' | 'canvas' | 'frame' | 'draw'
|
* 'id' | 'contractVersion' | 'capabilities' | 'canvas' | 'frame' | 'draw'
|
||||||
* | 'camera' | 'query' | 'capture' | 'resize' | 'destroy'>}
|
* | 'camera' | 'query' | 'capture' | 'on' | 'resize' | 'destroy'>}
|
||||||
* TransitionalRenderer
|
* TransitionalRenderer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -103,6 +103,7 @@ export function createCesiumRendererPlugin({
|
|||||||
camera: ports.camera,
|
camera: ports.camera,
|
||||||
query: ports.query,
|
query: ports.query,
|
||||||
capture: ports.capture,
|
capture: ports.capture,
|
||||||
|
on: ports.on,
|
||||||
resize() {
|
resize() {
|
||||||
viewer.resize();
|
viewer.resize();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -347,7 +347,6 @@ const EXPECTED_MISSING = [
|
|||||||
'renderer.effects',
|
'renderer.effects',
|
||||||
'renderer.globe',
|
'renderer.globe',
|
||||||
'renderer.input',
|
'renderer.input',
|
||||||
'renderer.on',
|
|
||||||
'renderer.quality',
|
'renderer.quality',
|
||||||
'renderer.readPixels',
|
'renderer.readPixels',
|
||||||
'renderer.releaseUnused',
|
'renderer.releaseUnused',
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const CESIUM_CHECKS = new Set([
|
|||||||
'query.projectMany',
|
'query.projectMany',
|
||||||
'query.rays',
|
'query.rays',
|
||||||
'query.withoutCapability',
|
'query.withoutCapability',
|
||||||
|
'renderer.listeners',
|
||||||
'renderer.whenSettled',
|
'renderer.whenSettled',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
+93
-16
@@ -21,6 +21,61 @@ import {
|
|||||||
renderPending,
|
renderPending,
|
||||||
} from './internals.js';
|
} from './internals.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where a listener's throw goes with no `error` listener: the platform's
|
||||||
|
* `reportError` (or a rethrow on a microtask), as a browser reports an event
|
||||||
|
* listener's exception, loud, while the frame and the other listeners carry
|
||||||
|
* on.
|
||||||
|
*/
|
||||||
|
function uncaught(error) {
|
||||||
|
if (typeof globalThis.reportError === 'function')
|
||||||
|
globalThis.reportError(error);
|
||||||
|
else
|
||||||
|
queueMicrotask(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One subscription list under the contract's listener rules: subscription
|
||||||
|
* order, one removed during delivery does not run, one added during
|
||||||
|
* delivery waits for the next event, and a throw goes to `onThrow` while
|
||||||
|
* the rest still run. Each subscription is its own entry, so a callback
|
||||||
|
* subscribed twice runs twice.
|
||||||
|
* @param {(error: unknown) => void} onThrow
|
||||||
|
*/
|
||||||
|
export function listenerList(onThrow) {
|
||||||
|
const entries = [];
|
||||||
|
return {
|
||||||
|
get size() {
|
||||||
|
return entries.length;
|
||||||
|
},
|
||||||
|
on(cb) {
|
||||||
|
const entry = { cb, live: true };
|
||||||
|
entries.push(entry);
|
||||||
|
return () => {
|
||||||
|
entry.live = false;
|
||||||
|
const i = entries.indexOf(entry);
|
||||||
|
if (i >= 0) entries.splice(i, 1);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fire(...args) {
|
||||||
|
for (const entry of entries.slice()) {
|
||||||
|
if (!entry.live) continue;
|
||||||
|
try {
|
||||||
|
entry.cb(...args);
|
||||||
|
} catch (error) {
|
||||||
|
onThrow(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
for (const entry of entries) entry.live = false;
|
||||||
|
entries.length = 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** The contract's colour as Cesium's. */
|
/** The contract's colour as Cesium's. */
|
||||||
export function toColor(color, alpha = 1) {
|
export function toColor(color, alpha = 1) {
|
||||||
if (color === undefined || color === null)
|
if (color === undefined || color === null)
|
||||||
@@ -127,7 +182,27 @@ export function createCesiumPorts(widget, { capabilities }) {
|
|||||||
const camera = scene.camera;
|
const camera = scene.camera;
|
||||||
const sets = new Map();
|
const sets = new Map();
|
||||||
|
|
||||||
const frameListeners = { preUpdate: [], preRender: [], postRender: [] };
|
// An error listener's own throw is uncaught, never reported again.
|
||||||
|
const rendererListeners = {
|
||||||
|
error: listenerList(uncaught),
|
||||||
|
contextLost: listenerList(report),
|
||||||
|
contextRestored: listenerList(report),
|
||||||
|
};
|
||||||
|
/** Hands a listener's throw to the renderer's `error` listeners. */
|
||||||
|
function report(error) {
|
||||||
|
if (rendererListeners.error.size === 0) uncaught(error);
|
||||||
|
else rendererListeners.error.fire(error);
|
||||||
|
}
|
||||||
|
// Cesium cannot bring a lost context back, so it never sends
|
||||||
|
// `contextRestored`.
|
||||||
|
const contextLost = () => rendererListeners.contextLost.fire();
|
||||||
|
scene.canvas?.addEventListener?.('webglcontextlost', contextLost);
|
||||||
|
|
||||||
|
const frameListeners = {
|
||||||
|
preUpdate: listenerList(report),
|
||||||
|
preRender: listenerList(report),
|
||||||
|
postRender: listenerList(report),
|
||||||
|
};
|
||||||
let frames = 0;
|
let frames = 0;
|
||||||
let rendered = 0;
|
let rendered = 0;
|
||||||
// Every tick raises preUpdate, drawn or not; the phases of a tick share
|
// Every tick raises preUpdate, drawn or not; the phases of a tick share
|
||||||
@@ -156,7 +231,7 @@ export function createCesiumPorts(widget, { capabilities }) {
|
|||||||
frames += 1;
|
frames += 1;
|
||||||
};
|
};
|
||||||
const onPhase = (phase) => (_scene, time) => {
|
const onPhase = (phase) => (_scene, time) => {
|
||||||
if (frameListeners[phase].length === 0) return;
|
if (frameListeners[phase].size === 0) return;
|
||||||
const t = {
|
const t = {
|
||||||
frame: frames,
|
frame: frames,
|
||||||
time: Cesium.JulianDate.toDate(
|
time: Cesium.JulianDate.toDate(
|
||||||
@@ -177,7 +252,7 @@ export function createCesiumPorts(widget, { capabilities }) {
|
|||||||
});
|
});
|
||||||
liveTick = t;
|
liveTick = t;
|
||||||
}
|
}
|
||||||
for (const cb of frameListeners[phase].slice()) cb(t);
|
frameListeners[phase].fire(t);
|
||||||
};
|
};
|
||||||
// Reasons given before a frame starts drawing are answered by it; one
|
// Reasons given before a frame starts drawing are answered by it; one
|
||||||
// given while it draws waits for the next, as Cesium's own request does.
|
// given while it draws waits for the next, as Cesium's own request does.
|
||||||
@@ -270,11 +345,9 @@ export function createCesiumPorts(widget, { capabilities }) {
|
|||||||
|
|
||||||
const frame = {
|
const frame = {
|
||||||
on(phase, cb) {
|
on(phase, cb) {
|
||||||
frameListeners[phase].push(cb);
|
const list = frameListeners[phase];
|
||||||
return () => {
|
if (!list) throw new TypeError(`Unknown frame phase: ${phase}`);
|
||||||
const i = frameListeners[phase].indexOf(cb);
|
return list.on(cb);
|
||||||
if (i >= 0) frameListeners[phase].splice(i, 1);
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
requestRender(reason = 'unspecified') {
|
requestRender(reason = 'unspecified') {
|
||||||
// During a draw the reasons it answers are spoken for: a reason given
|
// During a draw the reasons it answers are spoken for: a reason given
|
||||||
@@ -333,7 +406,7 @@ export function createCesiumPorts(widget, { capabilities }) {
|
|||||||
function makeSet(kind, spec, build) {
|
function makeSet(kind, spec, build) {
|
||||||
if (sets.has(spec.id)) throw new Error(`a set with id ${spec.id} is live`);
|
if (sets.has(spec.id)) throw new Error(`a set with id ${spec.id} is live`);
|
||||||
const items = new Map();
|
const items = new Map();
|
||||||
const listeners = { ready: [] };
|
const listeners = { ready: listenerList(report) };
|
||||||
const record = { kind, spec, items, listeners, host: null, pickIds: [] };
|
const record = { kind, spec, items, listeners, host: null, pickIds: [] };
|
||||||
// Each build records its items' pick ids, for height queries to see
|
// Each build records its items' pick ids, for height queries to see
|
||||||
// through.
|
// through.
|
||||||
@@ -381,12 +454,7 @@ export function createCesiumPorts(widget, { capabilities }) {
|
|||||||
scene.requestRender();
|
scene.requestRender();
|
||||||
},
|
},
|
||||||
on(event, cb) {
|
on(event, cb) {
|
||||||
listeners[event]?.push(cb);
|
return listeners[event]?.on(cb) ?? (() => {});
|
||||||
return () => {
|
|
||||||
const list = listeners[event];
|
|
||||||
const i = list?.indexOf(cb) ?? -1;
|
|
||||||
if (i >= 0) list.splice(i, 1);
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
dispose() {
|
dispose() {
|
||||||
if (record.host && !record.host.isDestroyed?.())
|
if (record.host && !record.host.isDestroyed?.())
|
||||||
@@ -702,7 +770,7 @@ export function createCesiumPorts(widget, { capabilities }) {
|
|||||||
host.add(model);
|
host.add(model);
|
||||||
model.readyEvent.addEventListener(() => {
|
model.readyEvent.addEventListener(() => {
|
||||||
settle(false);
|
settle(false);
|
||||||
for (const cb of record.listeners.ready.slice()) cb(item.id);
|
record.listeners.ready.fire(item.id);
|
||||||
});
|
});
|
||||||
model.errorEvent.addEventListener(() => settle(true));
|
model.errorEvent.addEventListener(() => settle(true));
|
||||||
scene.requestRender();
|
scene.requestRender();
|
||||||
@@ -1092,6 +1160,11 @@ export function createCesiumPorts(widget, { capabilities }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
/** The renderer's own events: `error`, `contextLost`, `contextRestored`. */
|
||||||
|
on(event, cb) {
|
||||||
|
return rendererListeners[event]?.on(cb) ?? (() => {});
|
||||||
|
},
|
||||||
|
report,
|
||||||
frame,
|
frame,
|
||||||
draw,
|
draw,
|
||||||
camera: cameraPort,
|
camera: cameraPort,
|
||||||
@@ -1106,6 +1179,10 @@ export function createCesiumPorts(widget, { capabilities }) {
|
|||||||
// The tick under way is no longer seen through: it is not claimed.
|
// The tick under way is no longer seen through: it is not claimed.
|
||||||
settleTick(false);
|
settleTick(false);
|
||||||
for (const off of removers) off();
|
for (const off of removers) off();
|
||||||
|
scene.canvas?.removeEventListener?.('webglcontextlost', contextLost);
|
||||||
|
// A listener that disposes the renderer stops the rest of its event.
|
||||||
|
for (const list of Object.values(rendererListeners)) list.clear();
|
||||||
|
for (const list of Object.values(frameListeners)) list.clear();
|
||||||
for (const done of [...pendingRenders]) done();
|
for (const done of [...pendingRenders]) done();
|
||||||
// Late model loads find no host and destroy themselves.
|
// Late model loads find no host and destroy themselves.
|
||||||
for (const record of sets.values()) record.host = null;
|
for (const record of sets.values()) record.host = null;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { cesiumCapabilities, createCesiumPorts } from './ports.js';
|
import {
|
||||||
|
cesiumCapabilities,
|
||||||
|
createCesiumPorts,
|
||||||
|
listenerList,
|
||||||
|
} from './ports.js';
|
||||||
|
|
||||||
test('capabilities follow what the scene supports', () => {
|
test('capabilities follow what the scene supports', () => {
|
||||||
assert.deepEqual(cesiumCapabilities({}), ['models']);
|
assert.deepEqual(cesiumCapabilities({}), ['models']);
|
||||||
@@ -27,6 +31,48 @@ test('a caller can ask for fewer capabilities, never more', () => {
|
|||||||
assert.deepEqual(cesiumCapabilities({}, new Set(['sampleHeight'])), []);
|
assert.deepEqual(cesiumCapabilities({}, new Set(['sampleHeight'])), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a listener list keeps the listener rules', () => {
|
||||||
|
const errors = [];
|
||||||
|
const list = listenerList((error) => errors.push(error));
|
||||||
|
const seen = [];
|
||||||
|
let offB = () => {};
|
||||||
|
list.on(() => {
|
||||||
|
seen.push('a');
|
||||||
|
offB();
|
||||||
|
list.on(() => seen.push('late'));
|
||||||
|
});
|
||||||
|
offB = list.on(() => seen.push('b'));
|
||||||
|
list.on(() => {
|
||||||
|
throw new Error('listener');
|
||||||
|
});
|
||||||
|
const c = () => seen.push('c');
|
||||||
|
list.on(c);
|
||||||
|
const offC2 = list.on(c);
|
||||||
|
list.fire();
|
||||||
|
assert.deepEqual(seen, ['a', 'c', 'c']);
|
||||||
|
assert.equal(errors.length, 1);
|
||||||
|
offC2();
|
||||||
|
seen.length = 0;
|
||||||
|
list.fire();
|
||||||
|
assert.deepEqual(seen, ['a', 'c', 'late']);
|
||||||
|
list.clear();
|
||||||
|
assert.equal(list.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a listener that clears its list stops the rest of the event', () => {
|
||||||
|
const list = listenerList((error) => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
const seen = [];
|
||||||
|
list.on(() => {
|
||||||
|
seen.push('a');
|
||||||
|
list.clear();
|
||||||
|
});
|
||||||
|
list.on(() => seen.push('b'));
|
||||||
|
list.fire();
|
||||||
|
assert.deepEqual(seen, ['a']);
|
||||||
|
});
|
||||||
|
|
||||||
/** A widget whose scene records what the query port asks of it. */
|
/** A widget whose scene records what the query port asks of it. */
|
||||||
function fakeWidget(overrides = {}) {
|
function fakeWidget(overrides = {}) {
|
||||||
const listeners = new Set();
|
const listeners = new Set();
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ export async function createCesiumSceneRenderer({
|
|||||||
capture: ports.capture,
|
capture: ports.capture,
|
||||||
whenSettled,
|
whenSettled,
|
||||||
resize: () => widget.resize(),
|
resize: () => widget.resize(),
|
||||||
|
on: ports.on,
|
||||||
destroy() {
|
destroy() {
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
for (const finish of [...watching]) finish('ended');
|
for (const finish of [...watching]) finish('ended');
|
||||||
|
|||||||
Reference in new issue
Block a user