mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(globe): engine members and the viewer bridge for the shell
Contract: camera.position(result?) and camera.height() for per-frame readers, render.request(reason?), maps.releasePhotorealTiles, sources with a caller-owned ready provider, isGlobeEngine. engine.maps now hides every tileset it has shown, lets the last imagery/photoreal call win, and setPhotorealTiles(null) restores the last base imagery and its credit instead of leaving the photoreal credit on a bare globe. Cesium engine: reads the Viewer camera/canvas aliases, detaches bridged events through the remover Cesium returns, raises frame timestamps without allocating, re-aims the horizon occluder only when the camera moved, and renders the scene directly when the governor governs another viewer (or none yet). engineFromViewer returns the registered scene engine of a viewer, else builds and caches one. The render governor stays the Cesium render backend under engine.render and gains isRenderGovernorInstalled.
This commit is contained in:
1 parent
a5c98c6b07
commit
58a196374f
26 files changed
+657
-45
No files matched your search
@@ -734,6 +734,8 @@
|
||||
"src/globe/cesium/credits.test.mjs",
|
||||
"src/globe/cesium/maps.test.mjs",
|
||||
"src/globe/cesium/createCesiumEngine.test.mjs",
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
"src/globe/cesium/engineFromViewer.test.mjs",
|
||||
"src/globe/cesium/testing/fakeViewer.mjs",
|
||||
"scripts/qa-globe-contract.mjs",
|
||||
"src/app/globeEngine.js",
|
||||
|
||||
@@ -1751,6 +1751,7 @@
|
||||
"src/globe/cesium/projector.js",
|
||||
"src/globe/cesium/render.js",
|
||||
"src/globe/cesium/surface.js",
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
"src/sources/sourceSlot.js",
|
||||
"src/app/sources.js",
|
||||
"src/app/startupChrome.js",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
|
||||
import { registerViewerEngine } from '../globe/cesium/engineFromViewer.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
|
||||
* teardown to the scene lifetime; engineFromViewer(viewer) returns it while
|
||||
* it lives. 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
|
||||
@@ -18,6 +20,10 @@ export function createSceneEngine(
|
||||
{ tilesetRef, defer, createEngine = createCesiumEngine },
|
||||
) {
|
||||
const engine = createEngine(viewer, { tilesetRef });
|
||||
defer(() => engine.destroy());
|
||||
const unregister = registerViewerEngine(viewer, engine);
|
||||
defer(() => {
|
||||
unregister();
|
||||
engine.destroy();
|
||||
});
|
||||
return engine;
|
||||
}
|
||||
@@ -65,3 +65,24 @@ test('the debug handle exposes the engine beside the viewer', () => {
|
||||
);
|
||||
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 () => {
|
||||
const { engineFromViewer } =
|
||||
await import('../globe/cesium/engineFromViewer.js');
|
||||
const { createFakeEngine } = await import('../globe/testing/fakeEngine.js');
|
||||
const viewer = { name: 'viewer' };
|
||||
const cleanups = [];
|
||||
const engine = createSceneEngine(viewer, {
|
||||
tilesetRef: null,
|
||||
defer: (dispose) => cleanups.push(dispose),
|
||||
createEngine: () => createFakeEngine(),
|
||||
});
|
||||
assert.equal(engineFromViewer(viewer), engine);
|
||||
for (const cleanup of cleanups.reverse()) cleanup();
|
||||
assert.equal(engine.fake.destroyed, true);
|
||||
assert.throws(
|
||||
() => engineFromViewer(viewer),
|
||||
/createCesiumEngine needs a viewer/,
|
||||
'no longer registered: a fresh one would be built',
|
||||
);
|
||||
});
|
||||
@@ -54,7 +54,8 @@ const cesiumOffset = (offset) =>
|
||||
* @returns {{ camera: import('../contract.js').GlobeCamera, destroy: () => void }}
|
||||
*/
|
||||
export function createCesiumCamera(viewer) {
|
||||
const cesium = viewer.scene.camera;
|
||||
// Viewer.camera is scene.camera; the public alias is what shell callers read.
|
||||
const cesium = viewer.camera ?? viewer.scene.camera;
|
||||
const bridges = {
|
||||
changed: bridgeEvent(cesium.changed),
|
||||
moveStart: bridgeEvent(cesium.moveStart),
|
||||
@@ -110,6 +111,15 @@ export function createCesiumCamera(viewer) {
|
||||
fovy: cesium.frustum.fovy,
|
||||
};
|
||||
},
|
||||
position(result) {
|
||||
const { x, y, z } = cesium.positionWC;
|
||||
if (!result) return geo.cartesian3.create(x, y, z);
|
||||
result.x = x;
|
||||
result.y = y;
|
||||
result.z = z;
|
||||
return result;
|
||||
},
|
||||
height: () => cesium.positionCartographic.height,
|
||||
setView(view) {
|
||||
if (!view?.destination)
|
||||
throw new TypeError('setView needs a destination');
|
||||
|
||||
@@ -346,3 +346,23 @@ test('rectangle destinations become a Cesium.Rectangle in flyTo and setView', ()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('position fills a result from positionWC and height reads the camera cartographic', () => {
|
||||
const { camera, cesium } = setup();
|
||||
cesium.positionCartographic = new Cesium.Cartographic(0.1, 0.2, 1234.5);
|
||||
const result = geo.cartesian3.create();
|
||||
assert.equal(camera.position(result), result);
|
||||
assert.deepEqual({ ...result }, { x: 7e6, y: 1e5, z: 2e5 });
|
||||
const fresh = camera.position();
|
||||
notCesium(fresh, 'position()');
|
||||
assert.notEqual(fresh, cesium.positionWC);
|
||||
assert.deepEqual({ ...fresh }, { x: 7e6, y: 1e5, z: 2e5 });
|
||||
assert.equal(camera.height(), 1234.5);
|
||||
});
|
||||
|
||||
test('the camera is the viewer camera (Viewer.camera is scene.camera)', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const own = { ...viewer.camera, positionWC: new Cesium.Cartesian3(1, 2, 3) };
|
||||
const { camera } = createCesiumCamera({ ...viewer, camera: own });
|
||||
assert.deepEqual({ ...camera.position() }, { x: 1, y: 2, z: 3 });
|
||||
});
|
||||
@@ -31,7 +31,8 @@ import { createCesiumSurface } from './surface.js';
|
||||
*/
|
||||
export function createCesiumEngine(viewer, options = {}) {
|
||||
if (!viewer?.scene) throw new TypeError('createCesiumEngine needs a viewer');
|
||||
const { scene } = viewer;
|
||||
// Viewer.canvas is scene.canvas; the engine reads the public alias.
|
||||
const canvas = viewer.canvas ?? viewer.scene.canvas;
|
||||
const render = createCesiumRender(viewer, options);
|
||||
const parts = {
|
||||
camera: createCesiumCamera(viewer),
|
||||
@@ -44,7 +45,7 @@ export function createCesiumEngine(viewer, options = {}) {
|
||||
credits: createCesiumCredits(viewer),
|
||||
maps: createCesiumMaps(viewer, {
|
||||
...options,
|
||||
requestRender: () => render.render.request(),
|
||||
requestRender: (reason) => render.render.request(reason),
|
||||
}),
|
||||
};
|
||||
let destroyed = false;
|
||||
@@ -52,10 +53,10 @@ export function createCesiumEngine(viewer, options = {}) {
|
||||
kind: 'cesium',
|
||||
host: {
|
||||
container: viewer.container,
|
||||
canvas: scene.canvas,
|
||||
canvas,
|
||||
size: () => ({
|
||||
width: scene.canvas.clientWidth,
|
||||
height: scene.canvas.clientHeight,
|
||||
width: canvas.clientWidth,
|
||||
height: canvas.clientHeight,
|
||||
}),
|
||||
},
|
||||
...Object.fromEntries(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { assertGlobeEngine } from '../contract.js';
|
||||
import {
|
||||
_resetRenderGovernorForTest,
|
||||
@@ -85,3 +86,53 @@ test('destroy releases holds, listeners, handlers and engine-owned map state', a
|
||||
assert.deepEqual(viewer.staticCredits, []);
|
||||
assert.deepEqual(viewer.imageryList, []);
|
||||
});
|
||||
|
||||
test('the engine reads the Viewer aliases and builds on a minimal viewer', () => {
|
||||
// Viewer.camera/.canvas are the scene's; a viewer-shaped object that offers
|
||||
// only the aliases, a container and a remover-returning postRender works.
|
||||
const listeners = new Set();
|
||||
const postRender = {
|
||||
addEventListener(fn) {
|
||||
listeners.add(fn);
|
||||
return () => listeners.delete(fn);
|
||||
},
|
||||
};
|
||||
let renders = 0;
|
||||
const viewer = {
|
||||
container: { kind: 'container' },
|
||||
canvas: { clientWidth: 320, clientHeight: 200 },
|
||||
camera: {
|
||||
positionWC: new Cesium.Cartesian3(7e6, 0, 0),
|
||||
positionCartographic: { height: 621863 },
|
||||
viewMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY),
|
||||
frustum: {
|
||||
projectionMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY),
|
||||
},
|
||||
},
|
||||
scene: { postRender, requestRender: () => renders++ },
|
||||
};
|
||||
const engine = createCesiumEngine(viewer);
|
||||
assert.equal(engine.host.canvas, viewer.canvas);
|
||||
assert.deepEqual(engine.host.size(), { width: 320, height: 200 });
|
||||
assert.equal(engine.camera.height(), 621863);
|
||||
assert.deepEqual(
|
||||
engine.projector.viewProjection(),
|
||||
Cesium.Matrix4.toArray(Cesium.Matrix4.IDENTITY),
|
||||
);
|
||||
assert.equal(
|
||||
engine.projector.horizonVisible({ x: 6378137, y: 0, z: 0 }),
|
||||
true,
|
||||
);
|
||||
const seen = [];
|
||||
const off = engine.frames.postRender.addEventListener((time) =>
|
||||
seen.push(time),
|
||||
);
|
||||
assert.equal(listeners.size, 1);
|
||||
for (const listener of listeners) listener();
|
||||
assert.equal(seen.length, 1);
|
||||
off();
|
||||
assert.equal(listeners.size, 0);
|
||||
engine.render.request();
|
||||
assert.equal(renders, 1);
|
||||
engine.destroy();
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// The bridge for shell callers that still hold only a Cesium viewer (layers
|
||||
// until phase 1c, standalone entries, tests): the engine of that viewer. The
|
||||
// app registers its scene engine (src/app/globeEngine.js); any other viewer
|
||||
// gets one built on first use and cached. Building one changes nothing in
|
||||
// the viewer, and it is never destroyed here: its subscriptions live only as
|
||||
// long as their callers keep them.
|
||||
|
||||
import { isGlobeEngine } from '../contract.js';
|
||||
import { createCesiumEngine } from './createCesiumEngine.js';
|
||||
|
||||
const engines = new WeakMap();
|
||||
|
||||
/**
|
||||
* Make `engine` the one returned for `viewer`.
|
||||
* @returns {() => void} Unregister (only while it is still the registered one).
|
||||
*/
|
||||
export function registerViewerEngine(viewer, engine) {
|
||||
engines.set(viewer, engine);
|
||||
return () => {
|
||||
if (engines.get(viewer) === engine) engines.delete(viewer);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object | null | undefined} viewer A Cesium viewer, or an engine
|
||||
* (returned as is).
|
||||
* @param {Parameters<typeof createCesiumEngine>[1]} [options] Used only when
|
||||
* an engine has to be built.
|
||||
* @returns {import('../contract.js').GlobeEngine | null}
|
||||
*/
|
||||
export function engineFromViewer(viewer, options) {
|
||||
if (viewer == null) return null;
|
||||
if (isGlobeEngine(viewer)) return viewer;
|
||||
let engine = engines.get(viewer);
|
||||
if (!engine) {
|
||||
engine = createCesiumEngine(viewer, options);
|
||||
engines.set(viewer, engine);
|
||||
}
|
||||
return engine;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { isGlobeEngine } from '../contract.js';
|
||||
import { createFakeEngine } from '../testing/fakeEngine.js';
|
||||
import { engineFromViewer, registerViewerEngine } from './engineFromViewer.js';
|
||||
import { createFakeViewer } from './testing/fakeViewer.mjs';
|
||||
|
||||
test('a viewer without a registered engine gets one built once and cached', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const engine = engineFromViewer(viewer);
|
||||
assert.equal(isGlobeEngine(engine), true);
|
||||
assert.equal(engine.kind, 'cesium');
|
||||
assert.equal(engineFromViewer(viewer), engine);
|
||||
assert.deepEqual(viewer.calls, [], 'building it changes nothing');
|
||||
assert.notEqual(engineFromViewer(createFakeViewer()), engine);
|
||||
});
|
||||
|
||||
test('the engine registered for a viewer is the one returned', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const engine = createFakeEngine();
|
||||
const unregister = registerViewerEngine(viewer, engine);
|
||||
assert.equal(engineFromViewer(viewer), engine);
|
||||
unregister();
|
||||
const built = engineFromViewer(viewer);
|
||||
assert.notEqual(built, engine);
|
||||
unregister();
|
||||
assert.equal(
|
||||
engineFromViewer(viewer),
|
||||
built,
|
||||
'a stale unregister keeps the new one',
|
||||
);
|
||||
});
|
||||
|
||||
test('an engine passes through, and no viewer is no engine', () => {
|
||||
const engine = createFakeEngine();
|
||||
assert.equal(engineFromViewer(engine), engine);
|
||||
assert.equal(engineFromViewer(null), null);
|
||||
assert.equal(engineFromViewer(undefined), null);
|
||||
});
|
||||
|
||||
test('isGlobeEngine tells engines from viewers', () => {
|
||||
assert.equal(isGlobeEngine(createFakeEngine()), true);
|
||||
assert.equal(isGlobeEngine(createFakeViewer()), false);
|
||||
assert.equal(isGlobeEngine(null), false);
|
||||
assert.equal(isGlobeEngine({ kind: 'cesium' }), false);
|
||||
});
|
||||
+21
-11
@@ -6,23 +6,33 @@
|
||||
import { createEngineEvent } from '../contract.js';
|
||||
|
||||
/**
|
||||
* @param {{ addEventListener: Function, removeEventListener: Function }} source A Cesium.Event.
|
||||
* @param {(...args: any[]) => any[]} [mapArgs] Cesium arguments → contract arguments.
|
||||
* @param {{ addEventListener: (listener: Function) => () => void }} source A
|
||||
* Cesium.Event (its addEventListener returns the remover the bridge uses).
|
||||
* @param {((...args: any[]) => any[]) | null} [mapArgs] Cesium arguments →
|
||||
* contract arguments.
|
||||
* @param {{ unary?: (...args: any[]) => any }} [options] `unary` maps the
|
||||
* Cesium arguments to the one contract argument without building an
|
||||
* argument list (for per-frame events).
|
||||
* @returns {{ event: import('../contract.js').EngineEvent, detach: () => void }}
|
||||
*/
|
||||
export function bridgeEvent(source, mapArgs) {
|
||||
export function bridgeEvent(source, mapArgs, { unary } = {}) {
|
||||
const event = createEngineEvent();
|
||||
const forward = mapArgs
|
||||
? (...args) => event.raiseEvent(...mapArgs(...args))
|
||||
: (...args) => event.raiseEvent(...args);
|
||||
let attached = false;
|
||||
const forward = unary
|
||||
? (a, b, c) => event.raiseEvent(unary(a, b, c))
|
||||
: mapArgs
|
||||
? (...args) => event.raiseEvent(...mapArgs(...args))
|
||||
: (...args) => event.raiseEvent(...args);
|
||||
let removeForward = null;
|
||||
let detached = false;
|
||||
const sync = () => {
|
||||
const wanted = !detached && event.numberOfListeners > 0;
|
||||
if (wanted === attached) return;
|
||||
attached = wanted;
|
||||
if (wanted) source.addEventListener(forward);
|
||||
else source.removeEventListener(forward);
|
||||
if (wanted === (removeForward !== null)) return;
|
||||
if (wanted) removeForward = source.addEventListener(forward);
|
||||
else {
|
||||
const remove = removeForward;
|
||||
removeForward = null;
|
||||
remove();
|
||||
}
|
||||
};
|
||||
const add = event.addEventListener;
|
||||
const remove = event.removeEventListener;
|
||||
|
||||
@@ -61,3 +61,35 @@ test('detach drops the Cesium subscription for good', () => {
|
||||
assert.equal(source.numberOfListeners, 0);
|
||||
source.raiseEvent();
|
||||
});
|
||||
|
||||
test('a bridge detaches through the remover the source returned', () => {
|
||||
// Cesium.Event.addEventListener returns a remover; the bridge relies on it
|
||||
// alone, so event-like sources without removeEventListener work too.
|
||||
const listeners = new Set();
|
||||
const source = {
|
||||
addEventListener(fn) {
|
||||
listeners.add(fn);
|
||||
return () => listeners.delete(fn);
|
||||
},
|
||||
};
|
||||
const { event, detach } = bridgeEvent(source);
|
||||
const remove = event.addEventListener(() => {});
|
||||
assert.equal(listeners.size, 1);
|
||||
remove();
|
||||
assert.equal(listeners.size, 0);
|
||||
event.addEventListener(() => {});
|
||||
detach();
|
||||
assert.equal(listeners.size, 0);
|
||||
});
|
||||
|
||||
test('a unary bridge raises one mapped value', () => {
|
||||
const source = new Cesium.Event();
|
||||
const { event, detach } = bridgeEvent(source, null, {
|
||||
unary: (scene, time) => time * 2,
|
||||
});
|
||||
const seen = [];
|
||||
event.addEventListener((...values) => seen.push(values));
|
||||
source.raiseEvent({}, 4);
|
||||
assert.deepEqual(seen, [[8]]);
|
||||
detach();
|
||||
});
|
||||
@@ -4,9 +4,23 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { bridgeEvent } from './events.js';
|
||||
|
||||
const toMilliseconds = (scene, time) => [
|
||||
Cesium.JulianDate.toDate(time).getTime(),
|
||||
];
|
||||
const scratchGregorian = new Cesium.GregorianDate();
|
||||
|
||||
// JulianDate.toDate(time).getTime() without the Date: frame events must not
|
||||
// allocate. A raise without a time (not Cesium's own) reads the clock.
|
||||
function unixMilliseconds(scene, time) {
|
||||
if (!time) return Date.now();
|
||||
const date = Cesium.JulianDate.toGregorianDate(time, scratchGregorian);
|
||||
return Date.UTC(
|
||||
date.year,
|
||||
date.month - 1,
|
||||
date.day,
|
||||
date.hour,
|
||||
date.minute,
|
||||
date.isLeapSecond ? date.second - 1 : date.second,
|
||||
date.millisecond,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cesium.Viewer} viewer
|
||||
@@ -16,7 +30,7 @@ export function createCesiumFrames(viewer) {
|
||||
const { scene } = viewer;
|
||||
const bridges = ['preUpdate', 'preRender', 'postRender'].map((name) => [
|
||||
name,
|
||||
bridgeEvent(scene[name], toMilliseconds),
|
||||
bridgeEvent(scene[name], null, { unary: unixMilliseconds }),
|
||||
]);
|
||||
return {
|
||||
frames: Object.fromEntries(
|
||||
|
||||
@@ -29,3 +29,29 @@ test('frame events bridge the scene events and pass a millisecond timestamp', ()
|
||||
for (const name of ['preUpdate', 'preRender', 'postRender'])
|
||||
assert.equal(scene[name].numberOfListeners, 0);
|
||||
});
|
||||
|
||||
test('timestamps match JulianDate.toDate, and a raise without a time reads the clock', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { frames, destroy } = createCesiumFrames(viewer);
|
||||
const seen = [];
|
||||
frames.postRender.addEventListener((time) => seen.push(time));
|
||||
const times = [
|
||||
Cesium.JulianDate.fromDate(new Date(Date.UTC(1999, 0, 1, 0, 0, 0, 1))),
|
||||
Cesium.JulianDate.fromIso8601('2016-12-31T23:59:60.5Z'),
|
||||
Cesium.JulianDate.addSeconds(
|
||||
Cesium.JulianDate.fromDate(new Date(Date.UTC(2026, 9, 15))),
|
||||
0.1234,
|
||||
new Cesium.JulianDate(),
|
||||
),
|
||||
];
|
||||
for (const time of times)
|
||||
viewer.scene.postRender.raiseEvent(viewer.scene, time);
|
||||
assert.deepEqual(
|
||||
seen,
|
||||
times.map((time) => Cesium.JulianDate.toDate(time).getTime()),
|
||||
);
|
||||
const before = Date.now();
|
||||
viewer.scene.postRender.raiseEvent();
|
||||
assert.ok(seen.at(-1) >= before && seen.at(-1) <= Date.now());
|
||||
destroy();
|
||||
});
|
||||
+48
-11
@@ -82,20 +82,31 @@ export function createCesiumMaps(
|
||||
const providers = new Map();
|
||||
let ownedTileset = null;
|
||||
let shownTileset = null;
|
||||
const generations = { imagery: 0, terrain: 0, photoreal: 0 };
|
||||
// Every tileset shown through the engine, so a switch hides all of them;
|
||||
// `added` are those the engine put into scene.primitives itself.
|
||||
const known = new Set();
|
||||
const added = new Set();
|
||||
// Imagery and photoreal share one generation: whichever was asked for last
|
||||
// decides what the globe shows. Terrain has its own.
|
||||
const generations = { display: 0, terrain: 0 };
|
||||
let baseSource = null;
|
||||
let creditOwner = null;
|
||||
let destroyed = false;
|
||||
|
||||
const startupTileset = () =>
|
||||
(typeof tilesetRef === 'function' ? tilesetRef() : tilesetRef?.current) ??
|
||||
null;
|
||||
const tilesets = () =>
|
||||
[startupTileset(), shownTileset, ownedTileset].filter(
|
||||
[startupTileset(), shownTileset, ownedTileset, ...known].filter(
|
||||
(tileset, index, all) => tileset && all.indexOf(tileset) === index,
|
||||
);
|
||||
|
||||
// One provider per source, as MapSourceController caches them, so applying
|
||||
// the same source again keeps the live layer. Failures are not cached.
|
||||
// the same source again keeps the live layer. Failures are not cached. A
|
||||
// source carrying a ready `provider` belongs to its caller: it is neither
|
||||
// cached nor disposed here.
|
||||
function providerFor(source) {
|
||||
if (source?.provider) return Promise.resolve(source.provider);
|
||||
const key = imageryKey(source);
|
||||
if (providers.has(key)) return providers.get(key);
|
||||
const promise = Promise.resolve()
|
||||
@@ -108,13 +119,23 @@ export function createCesiumMaps(
|
||||
return promise;
|
||||
}
|
||||
|
||||
function showCredit(owner, html) {
|
||||
creditOwner = owner;
|
||||
mapScene.showCredit(html);
|
||||
}
|
||||
|
||||
function showBase(source, provider) {
|
||||
mapScene.showBaseImagery(provider);
|
||||
showCredit('imagery', imageryCredit(source));
|
||||
}
|
||||
|
||||
const maps = {
|
||||
async setBaseImagery(source) {
|
||||
const generation = ++generations.imagery;
|
||||
const generation = ++generations.display;
|
||||
const provider = await providerFor(source);
|
||||
if (destroyed || generation !== generations.imagery) return;
|
||||
mapScene.showBaseImagery(provider);
|
||||
mapScene.showCredit(imageryCredit(source));
|
||||
if (destroyed || generation !== generations.display) return;
|
||||
baseSource = { source, provider };
|
||||
showBase(source, provider);
|
||||
requestRender('map-imagery');
|
||||
},
|
||||
async setTerrain(source) {
|
||||
@@ -125,11 +146,17 @@ export function createCesiumMaps(
|
||||
requestRender('map-terrain');
|
||||
},
|
||||
async setPhotorealTiles(source) {
|
||||
const generation = ++generations.photoreal;
|
||||
const generation = ++generations.display;
|
||||
const { scene } = viewer;
|
||||
if (source == null) {
|
||||
mapScene.showTileset(null, tilesets());
|
||||
shownTileset = null;
|
||||
// The globe comes back with the last base imagery and its credit;
|
||||
// the photoreal credit goes.
|
||||
if (baseSource) {
|
||||
if (!mapScene.baseProvider)
|
||||
showBase(baseSource.source, baseSource.provider);
|
||||
} else if (creditOwner === 'photoreal') showCredit(null, null);
|
||||
mapScene.showGlobe(true);
|
||||
requestRender('map-photoreal');
|
||||
return;
|
||||
@@ -144,7 +171,7 @@ export function createCesiumMaps(
|
||||
tileset = loaded;
|
||||
owned = true;
|
||||
}
|
||||
if (destroyed || generation !== generations.photoreal) {
|
||||
if (destroyed || generation !== generations.display) {
|
||||
if (owned) dispose(tileset);
|
||||
return;
|
||||
}
|
||||
@@ -155,16 +182,26 @@ export function createCesiumMaps(
|
||||
}
|
||||
ownedTileset = tileset;
|
||||
}
|
||||
if (!scene.primitives.contains(tileset)) scene.primitives.add(tileset);
|
||||
if (!scene.primitives.contains(tileset)) {
|
||||
scene.primitives.add(tileset);
|
||||
if (!owned) added.add(tileset);
|
||||
}
|
||||
known.add(tileset);
|
||||
// As MapSourceController activates a 3D stack: no base layer, the
|
||||
// source's credit, one tileset shown and the globe hidden.
|
||||
mapScene.removeBaseImagery();
|
||||
mapScene.showCredit(source.credit);
|
||||
showCredit('photoreal', source.credit);
|
||||
shownTileset = tileset;
|
||||
mapScene.showTileset(tileset, tilesets());
|
||||
mapScene.showGlobe(false);
|
||||
requestRender('map-photoreal');
|
||||
},
|
||||
releasePhotorealTiles(source) {
|
||||
const tileset = source?.tileset;
|
||||
if (!tileset || !known.delete(tileset)) return;
|
||||
if (shownTileset === tileset) shownTileset = null;
|
||||
if (added.delete(tileset)) viewer.scene.primitives.remove(tileset);
|
||||
},
|
||||
photorealActive: () => tilesets().some((tileset) => tileset.show === true),
|
||||
};
|
||||
|
||||
|
||||
@@ -262,3 +262,119 @@ test('destroy removes the engine imagery layer and its credit', async () => {
|
||||
assert.deepEqual(viewer.imageryList, []);
|
||||
assert.deepEqual(viewer.staticCredits, []);
|
||||
});
|
||||
|
||||
const esriSource = (provider = { name: 'esri' }) => ({
|
||||
descriptor: { id: 'esri-imagery', kind: 'esri-imagery' },
|
||||
imagery: () => provider,
|
||||
credit: ESRI_ATTRIBUTION_HTML,
|
||||
});
|
||||
const credits = (viewer) => viewer.staticCredits.map((credit) => credit.html);
|
||||
|
||||
test('setPhotorealTiles(null) clears the photoreal credit and restores the base imagery', async () => {
|
||||
const { viewer, maps } = setup();
|
||||
const provider = { name: 'esri' };
|
||||
await maps.setBaseImagery(esriSource(provider));
|
||||
await maps.setPhotorealTiles({ tileset: tileset(), credit: 'Google' });
|
||||
assert.deepEqual(viewer.imageryList, []);
|
||||
assert.deepEqual(credits(viewer), ['Google']);
|
||||
await maps.setPhotorealTiles(null);
|
||||
assert.deepEqual(viewer.imageryList, [{ provider }], 'base layer back');
|
||||
assert.deepEqual(credits(viewer), [ESRI_ATTRIBUTION_HTML]);
|
||||
assert.equal(viewer.scene.globe.show, true);
|
||||
});
|
||||
|
||||
test('setPhotorealTiles(null) before any base imagery shows no credit', async () => {
|
||||
const { viewer, maps } = setup();
|
||||
await maps.setPhotorealTiles({ tileset: tileset(), credit: 'Google' });
|
||||
await maps.setPhotorealTiles(null);
|
||||
assert.deepEqual(credits(viewer), []);
|
||||
assert.deepEqual(viewer.imageryList, []);
|
||||
});
|
||||
|
||||
test('base imagery then setPhotorealTiles(null) keeps the base layer and its credit', async () => {
|
||||
const { viewer, maps } = setup();
|
||||
const provider = { name: 'esri' };
|
||||
await maps.setPhotorealTiles({ tileset: tileset(), credit: 'Google' });
|
||||
await maps.setBaseImagery(esriSource(provider));
|
||||
await maps.setPhotorealTiles(null);
|
||||
assert.deepEqual(viewer.imageryList, [{ provider }]);
|
||||
assert.equal(viewer.callsOf('imageryLayers.add').length, 1, 'not rebuilt');
|
||||
assert.deepEqual(credits(viewer), [ESRI_ATTRIBUTION_HTML]);
|
||||
});
|
||||
|
||||
test('a newer photoreal switch supersedes a pending imagery switch', async () => {
|
||||
const { viewer, maps } = setup();
|
||||
let resolve;
|
||||
const pending = maps.setBaseImagery({
|
||||
descriptor: { id: 'slow', kind: 'osm' },
|
||||
imagery: () => new Promise((done) => (resolve = done)),
|
||||
credit: 'slow credit',
|
||||
});
|
||||
await maps.setPhotorealTiles({ tileset: tileset(), credit: 'Google' });
|
||||
resolve({ name: 'slow' });
|
||||
await pending;
|
||||
assert.deepEqual(viewer.imageryList, []);
|
||||
assert.deepEqual(credits(viewer), ['Google']);
|
||||
assert.equal(viewer.scene.globe.show, false);
|
||||
});
|
||||
|
||||
test('switching between two tilesets hides the one shown before', async () => {
|
||||
const { maps } = setup();
|
||||
const first = tileset();
|
||||
const second = tileset();
|
||||
await maps.setPhotorealTiles({ tileset: first });
|
||||
await maps.setPhotorealTiles({ tileset: second });
|
||||
assert.equal(first.show, false);
|
||||
assert.equal(second.show, true);
|
||||
await maps.setPhotorealTiles(null);
|
||||
assert.equal(second.show, false);
|
||||
});
|
||||
|
||||
test('a source with a ready provider is shown as is and stays its caller’s', async () => {
|
||||
const { viewer, maps, destroy } = setup();
|
||||
let destroyed = 0;
|
||||
const provider = { name: 'owned', destroy: () => destroyed++ };
|
||||
const source = {
|
||||
descriptor: { id: 'osm', kind: 'osm' },
|
||||
provider,
|
||||
credit: 'c',
|
||||
};
|
||||
await maps.setBaseImagery(source);
|
||||
assert.deepEqual(viewer.imageryList, [{ provider }]);
|
||||
const other = { name: 'other' };
|
||||
await maps.setBaseImagery({ ...source, provider: other });
|
||||
assert.deepEqual(
|
||||
viewer.imageryList,
|
||||
[{ provider: other }],
|
||||
'not cached by id',
|
||||
);
|
||||
destroy();
|
||||
assert.equal(destroyed, 0, 'the engine does not dispose a caller provider');
|
||||
});
|
||||
|
||||
test('releasePhotorealTiles forgets a tileset and removes it from the scene', async () => {
|
||||
const { viewer, maps } = setup();
|
||||
const owned = tileset();
|
||||
await maps.setPhotorealTiles({ tileset: owned });
|
||||
await maps.setPhotorealTiles(null);
|
||||
maps.releasePhotorealTiles({ tileset: owned });
|
||||
assert.equal(viewer.primitiveList.includes(owned), false);
|
||||
assert.equal(maps.photorealActive(), false);
|
||||
owned.show = true;
|
||||
assert.equal(
|
||||
maps.photorealActive(),
|
||||
false,
|
||||
'no longer one of the engine tilesets',
|
||||
);
|
||||
maps.releasePhotorealTiles({ tileset: owned });
|
||||
assert.equal(viewer.callsOf('primitives.remove').length, 1, 'idempotent');
|
||||
});
|
||||
|
||||
test('releasePhotorealTiles leaves a tileset the engine did not add in the scene', async () => {
|
||||
const { viewer, maps } = setup();
|
||||
const startup = tileset(true);
|
||||
viewer.primitiveList.push(startup);
|
||||
await maps.setPhotorealTiles({ tileset: startup });
|
||||
maps.releasePhotorealTiles({ tileset: startup });
|
||||
assert.equal(viewer.primitiveList.includes(startup), true);
|
||||
});
|
||||
@@ -16,9 +16,16 @@ export function createCesiumProjector(
|
||||
} = {},
|
||||
) {
|
||||
const { scene } = viewer;
|
||||
// Viewer.camera is scene.camera; the engine reads the public alias.
|
||||
const camera = viewer.camera ?? scene.camera;
|
||||
const scratchWindow = new Cesium.Cartesian2();
|
||||
const scratchMatrix = new Cesium.Matrix4();
|
||||
const occluder = new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84);
|
||||
// The occluder is re-aimed only when the camera has moved: horizonVisible
|
||||
// runs per label per frame.
|
||||
let occluderX = Number.NaN;
|
||||
let occluderY = Number.NaN;
|
||||
let occluderZ = Number.NaN;
|
||||
|
||||
const projector = {
|
||||
worldToWindow(position, result) {
|
||||
@@ -33,7 +40,6 @@ export function createCesiumProjector(
|
||||
// The matrix Cesium's worldToWindowCoordinates uses: frustum projection ×
|
||||
// view. x, y and w do not depend on the multi-frustum near/far planes.
|
||||
viewProjection(result = geo.matrix4.create()) {
|
||||
const { camera } = scene;
|
||||
Cesium.Matrix4.multiply(
|
||||
camera.frustum.projectionMatrix,
|
||||
camera.viewMatrix,
|
||||
@@ -44,7 +50,17 @@ export function createCesiumProjector(
|
||||
return result;
|
||||
},
|
||||
horizonVisible(position) {
|
||||
occluder.cameraPosition = scene.camera.positionWC;
|
||||
const { positionWC } = camera;
|
||||
if (
|
||||
positionWC.x !== occluderX ||
|
||||
positionWC.y !== occluderY ||
|
||||
positionWC.z !== occluderZ
|
||||
) {
|
||||
occluder.cameraPosition = positionWC;
|
||||
occluderX = positionWC.x;
|
||||
occluderY = positionWC.y;
|
||||
occluderZ = positionWC.z;
|
||||
}
|
||||
return occluder.isPointVisible(position);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import {
|
||||
governorRequestRender,
|
||||
holdContinuousRender,
|
||||
isRenderGovernorInstalled,
|
||||
releaseContinuousRender,
|
||||
} from '../../renderGovernor.js';
|
||||
|
||||
@@ -12,17 +13,24 @@ export const RENDER_OWNER = 'globe-engine';
|
||||
|
||||
/**
|
||||
* @param {Cesium.Viewer} viewer
|
||||
* @param {{ requestRender?: (reason: string) => void }} [options]
|
||||
* @param {{ requestRender?: (reason: string) => void }} [options] Defaults to
|
||||
* the render governor when it governs this viewer, else the scene itself
|
||||
* (before the governor is installed, or on another viewer).
|
||||
* @returns {{ render: import('../contract.js').GlobeRender, destroy: () => void }}
|
||||
*/
|
||||
export function createCesiumRender(
|
||||
viewer,
|
||||
{ requestRender = governorRequestRender } = {},
|
||||
{
|
||||
requestRender = (reason) => {
|
||||
if (isRenderGovernorInstalled(viewer)) governorRequestRender(reason);
|
||||
else viewer.scene.requestRender?.();
|
||||
},
|
||||
} = {},
|
||||
) {
|
||||
let held = false;
|
||||
const render = {
|
||||
request() {
|
||||
requestRender(RENDER_OWNER);
|
||||
request(reason = RENDER_OWNER) {
|
||||
requestRender(reason);
|
||||
},
|
||||
setContinuous(enabled) {
|
||||
held = Boolean(enabled);
|
||||
|
||||
@@ -76,3 +76,27 @@ test('destroy releases the engine hold', () => {
|
||||
destroy();
|
||||
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
|
||||
});
|
||||
|
||||
test('request passes a caller reason through to the governor', () => {
|
||||
const viewer = createFakeViewer();
|
||||
installRenderGovernor(viewer);
|
||||
const { render } = createCesiumRender(viewer);
|
||||
render.request('world-overlay');
|
||||
assert.deepEqual(
|
||||
getRenderGovernorDiagnostics().recentRequests.map(({ reason }) => reason),
|
||||
['world-overlay'],
|
||||
);
|
||||
});
|
||||
|
||||
test('without a governor on its viewer, request renders the scene directly', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { render } = createCesiumRender(viewer);
|
||||
render.request();
|
||||
assert.equal(viewer.callsOf('scene.requestRender').length, 1);
|
||||
const governed = createFakeViewer();
|
||||
installRenderGovernor(governed);
|
||||
render.request();
|
||||
assert.equal(viewer.callsOf('scene.requestRender').length, 2);
|
||||
assert.equal(governed.callsOf('scene.requestRender').length, 0);
|
||||
assert.deepEqual(getRenderGovernorDiagnostics().recentRequests, []);
|
||||
});
|
||||
+31
-3
@@ -83,6 +83,11 @@
|
||||
*
|
||||
* @typedef {object} GlobeCamera
|
||||
* @property {() => CameraPose} pose Fresh values on every call.
|
||||
* @property {(result?: Cartesian3) => Cartesian3} position The pose's world
|
||||
* position; fills and returns `result` when given (no allocation, for
|
||||
* per-frame readers), else a fresh value.
|
||||
* @property {() => number} height The camera's height above the WGS84
|
||||
* ellipsoid (the pose's cartographic height), without building a pose.
|
||||
* @property {(view: { destination: Cartesian3 | Rectangle, orientation?: Orientation }) => void} setView
|
||||
* Missing angles default to heading 0, pitch −π/2, roll 0.
|
||||
* @property {(options: FlightOptions) => void} flyTo Cancels a running flight first.
|
||||
@@ -141,7 +146,8 @@
|
||||
* @property {() => boolean} tilesLoaded
|
||||
*
|
||||
* @typedef {object} GlobeRender
|
||||
* @property {() => void} request Renders a frame soon in request mode.
|
||||
* @property {(reason?: string) => void} request Renders a frame soon in
|
||||
* request mode; `reason` is for diagnostics only.
|
||||
* @property {(enabled: boolean) => void} setContinuous
|
||||
* Owns whatever the engine needs to render every frame or only on request
|
||||
* (Cesium: requestRenderMode and maximumRenderTimeChange = Infinity).
|
||||
@@ -167,10 +173,17 @@
|
||||
* As in Cesium, a listener with a modifier fires only while that key is
|
||||
* held and one without fires only while no modifier is held.
|
||||
* @property {{ addStatic: (html: string, options?: { onScreen?: boolean }) => () => void }} credits
|
||||
* @property {{ setBaseImagery: (source: object) => Promise<void>, setTerrain: (source: object) => Promise<void>, setPhotorealTiles: (source: object | null) => Promise<void>, photorealActive: () => boolean }} maps
|
||||
* @property {{ setBaseImagery: (source: object) => Promise<void>, setTerrain: (source: object) => Promise<void>, setPhotorealTiles: (source: object | null) => Promise<void>, releasePhotorealTiles: (source: object) => void, photorealActive: () => boolean }} maps
|
||||
* Sources are maps/registry.js descriptions; the engine builds its own
|
||||
* providers. Each setter settles once the source is on screen and rejects
|
||||
* when it cannot be built (the current source then stays).
|
||||
* when it cannot be built (the current source then stays). A source may
|
||||
* carry a ready engine `provider` its caller built and owns (the engine
|
||||
* neither caches nor disposes it). Base imagery and photoreal tiles share
|
||||
* one switch order: the one asked for last wins, a superseded call settles
|
||||
* without showing anything. setPhotorealTiles(null) shows the globe again
|
||||
* with the last base imagery and its credit (no credit when none was set).
|
||||
* releasePhotorealTiles forgets a tileset source shown before and takes it
|
||||
* out of the scene when the engine put it there; its caller disposes it.
|
||||
* @property {() => void} destroy
|
||||
*/
|
||||
|
||||
@@ -269,6 +282,8 @@ const SHAPE = {
|
||||
host: { container: isObject, canvas: isObject, size: isFunction },
|
||||
camera: {
|
||||
pose: isFunction,
|
||||
position: isFunction,
|
||||
height: isFunction,
|
||||
setView: isFunction,
|
||||
flyTo: isFunction,
|
||||
flyToSphere: isFunction,
|
||||
@@ -324,6 +339,7 @@ const SHAPE = {
|
||||
setBaseImagery: isFunction,
|
||||
setTerrain: isFunction,
|
||||
setPhotorealTiles: isFunction,
|
||||
releasePhotorealTiles: isFunction,
|
||||
photorealActive: isFunction,
|
||||
},
|
||||
};
|
||||
@@ -337,6 +353,18 @@ export const GLOBE_ENGINE_MEMBERS = Object.freeze([
|
||||
'destroy',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Whether a value is a globe engine rather than, say, a Cesium viewer (a
|
||||
* cheap structural test; assertGlobeEngine checks every member).
|
||||
*/
|
||||
export const isGlobeEngine = (value) =>
|
||||
isObject(value) &&
|
||||
ENGINE_KINDS.includes(value.kind) &&
|
||||
isObject(value.camera) &&
|
||||
isObject(value.projector) &&
|
||||
isObject(value.frames) &&
|
||||
isFunction(value.destroy);
|
||||
|
||||
/** Throws a TypeError naming every missing or mistyped member; returns the engine. */
|
||||
export function assertGlobeEngine(engine) {
|
||||
if (!isObject(engine)) throw new TypeError('GlobeEngine must be an object');
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
ENGINE_KINDS,
|
||||
GLOBE_ENGINE_MEMBERS,
|
||||
INPUT_TYPES,
|
||||
MODIFIERS,
|
||||
assertGlobeEngine,
|
||||
@@ -185,6 +186,25 @@ test('assertGlobeEngine accepts a complete engine and names every missing capabi
|
||||
assert.throws(() => assertGlobeEngine(null), TypeError);
|
||||
});
|
||||
|
||||
test('the contract carries what the shell infrastructure needs', () => {
|
||||
for (const member of [
|
||||
'camera.position',
|
||||
'camera.height',
|
||||
'maps.releasePhotorealTiles',
|
||||
])
|
||||
assert.ok(GLOBE_ENGINE_MEMBERS.includes(member), member);
|
||||
const engine = createFakeEngine();
|
||||
const broken = {
|
||||
...engine,
|
||||
camera: { ...engine.camera, position: undefined, height: 1 },
|
||||
maps: { ...engine.maps, releasePhotorealTiles: undefined },
|
||||
};
|
||||
assert.throws(
|
||||
() => assertGlobeEngine(broken),
|
||||
/camera\.position.*camera\.height.*maps\.releasePhotorealTiles/,
|
||||
);
|
||||
});
|
||||
|
||||
test('the fake engine passes the engine contract suite', async (t) => {
|
||||
const engine = createFakeEngine();
|
||||
const checks = createContractSuite(engine, {
|
||||
|
||||
@@ -176,6 +176,41 @@ export function createContractSuite(
|
||||
near(cartesian3.dot(pose.up, up), 1, 1e-9, 'up·expected');
|
||||
},
|
||||
],
|
||||
[
|
||||
'camera.position and camera.height: the pose position and height, without a pose',
|
||||
async () => {
|
||||
camera.setView({
|
||||
destination: cartesian3.fromDegrees(-3.7, 40.4, 12000),
|
||||
orientation: { heading: 0.4, pitch: -0.5, roll: 0 },
|
||||
});
|
||||
await settle();
|
||||
const pose = camera.pose();
|
||||
const result = cartesian3.create();
|
||||
const filled = camera.position(result);
|
||||
ensure(
|
||||
filled === result,
|
||||
'position(result) returns the result it filled',
|
||||
);
|
||||
near(
|
||||
cartesian3.distance(filled, pose.position),
|
||||
0,
|
||||
1e-6,
|
||||
'position(result) vs pose().position',
|
||||
);
|
||||
const fresh = camera.position();
|
||||
ensure(
|
||||
isCartesian3(fresh) && fresh !== filled,
|
||||
'position() returns a fresh Cartesian3',
|
||||
);
|
||||
near(
|
||||
cartesian3.distance(fresh, pose.position),
|
||||
0,
|
||||
1e-6,
|
||||
'position() vs pose().position',
|
||||
);
|
||||
near(camera.height(), pose.cartographic.height, 0.01, 'height()');
|
||||
},
|
||||
],
|
||||
[
|
||||
'camera.pose: cartographic, frame vectors and fovy are consistent',
|
||||
async () => {
|
||||
|
||||
@@ -602,6 +602,12 @@ export function createFakeEngine(options = {}) {
|
||||
fovy,
|
||||
};
|
||||
},
|
||||
position(result) {
|
||||
return result
|
||||
? cartesian3.clone(state.position, result)
|
||||
: copy3(state.position);
|
||||
},
|
||||
height: () => cartographic.fromCartesian(state.position, WGS84).height,
|
||||
setView(view) {
|
||||
record('camera.setView', [view]);
|
||||
if (!view?.destination)
|
||||
@@ -756,8 +762,8 @@ export function createFakeEngine(options = {}) {
|
||||
postRender: events['frames.postRender'],
|
||||
},
|
||||
render: {
|
||||
request() {
|
||||
record('render.request', []);
|
||||
request(reason) {
|
||||
record('render.request', reason === undefined ? [] : [reason]);
|
||||
fake.renderRequests++;
|
||||
},
|
||||
setContinuous(enabled) {
|
||||
@@ -825,6 +831,9 @@ export function createFakeEngine(options = {}) {
|
||||
record('maps.setPhotorealTiles', [source]);
|
||||
fake.maps.photoreal = source ?? null;
|
||||
},
|
||||
releasePhotorealTiles(source) {
|
||||
record('maps.releasePhotorealTiles', [source]);
|
||||
},
|
||||
photorealActive: () => fake.maps.photoreal != null,
|
||||
},
|
||||
destroy() {
|
||||
|
||||
@@ -353,3 +353,13 @@ test('fake drillPick records width and height; maps return promises', async () =
|
||||
await call;
|
||||
}
|
||||
});
|
||||
|
||||
test('fake camera position fills a result and height is the pose height', () => {
|
||||
const engine = createFakeEngine({ altitude: 2_000_000 });
|
||||
const result = geo.cartesian3.create();
|
||||
assert.equal(engine.camera.position(result), result);
|
||||
assert.ok(
|
||||
geo.cartesian3.distance(result, engine.camera.pose().position) < 1e-6,
|
||||
);
|
||||
assert.ok(Math.abs(engine.camera.height() - 2_000_000) < 0.01);
|
||||
});
|
||||
@@ -29,6 +29,13 @@
|
||||
* diagnostics read like a story.
|
||||
*
|
||||
* The governor is O(1) passive: no per-frame work of its own, ever.
|
||||
*
|
||||
* Globe engine: the governor is the Cesium engine's render backend.
|
||||
* `engine.render.request()/setContinuous()` (src/globe/cesium/render.js)
|
||||
* delegate here, so the governor never uses `engine.render` (that would be a
|
||||
* cycle) and keeps the viewer for what the contract leaves to the engine:
|
||||
* `scene.requestRenderMode`, `scene.maximumRenderTimeChange` and
|
||||
* `scene.requestRender()`. It needs no frame events (`engine.frames`).
|
||||
*/
|
||||
|
||||
let _viewer = null;
|
||||
@@ -129,6 +136,16 @@ export function getRenderGovernorDiagnostics() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the governor is installed on this viewer (its render requests then
|
||||
* go through `governorRequestRender`).
|
||||
* @param {object} viewer
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isRenderGovernorInstalled(viewer) {
|
||||
return _installed && viewer != null && _viewer === viewer;
|
||||
}
|
||||
|
||||
/** Release the installed viewer after its animation owners have stopped. */
|
||||
export function uninstallRenderGovernor(viewer) {
|
||||
if (_viewer !== viewer) return;
|
||||
|
||||
@@ -98,3 +98,15 @@ test('holds registered before install apply at install time', () => {
|
||||
releaseContinuousRender('flights');
|
||||
assert.equal(scene.requestRenderMode, true);
|
||||
});
|
||||
|
||||
test('isRenderGovernorInstalled names the one governed viewer', async () => {
|
||||
const { isRenderGovernorInstalled, uninstallRenderGovernor } = await import('./renderGovernor.js');
|
||||
const { viewer } = makeViewer();
|
||||
const other = makeViewer().viewer;
|
||||
assert.equal(isRenderGovernorInstalled(viewer), false);
|
||||
installRenderGovernor(viewer);
|
||||
assert.equal(isRenderGovernorInstalled(viewer), true);
|
||||
assert.equal(isRenderGovernorInstalled(other), false);
|
||||
uninstallRenderGovernor(viewer);
|
||||
assert.equal(isRenderGovernorInstalled(viewer), false);
|
||||
});
|
||||
Reference in new issue
Block a user