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
@@ -671,7 +671,7 @@
|
|||||||
"src/services/application.js",
|
"src/services/application.js",
|
||||||
"src/services/requests.js",
|
"src/services/requests.js",
|
||||||
"src/app/scene.js",
|
"src/app/scene.js",
|
||||||
"src/app/renderer.test.mjs",
|
"src/render/cesium/appRenderer.test.mjs",
|
||||||
"src/render/noCesiumOutsideRender.test.mjs",
|
"src/render/noCesiumOutsideRender.test.mjs",
|
||||||
"src/render/cesiumAllowList.json",
|
"src/render/cesiumAllowList.json",
|
||||||
"src/geo/geo.test.mjs",
|
"src/geo/geo.test.mjs",
|
||||||
|
|||||||
@@ -2421,7 +2421,8 @@
|
|||||||
"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/renderer.js",
|
"src/render/cesium/appRenderer.js",
|
||||||
|
"src/render/cesium/ports.js",
|
||||||
"src/render/contract/index.js",
|
"src/render/contract/index.js",
|
||||||
"src/sources/sourceSlot.js",
|
"src/sources/sourceSlot.js",
|
||||||
"src/app/sources.js",
|
"src/app/sources.js",
|
||||||
|
|||||||
+4
-1
@@ -1,7 +1,10 @@
|
|||||||
import { createApplicationOperations } from './operations.js';
|
import { createApplicationOperations } from './operations.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { installTrackpadPinchZoom } from '../app/viewer.js';
|
import { installTrackpadPinchZoom } from '../app/viewer.js';
|
||||||
import { cesiumRenderer, cesiumViewerOf } from './renderer.js';
|
import {
|
||||||
|
cesiumRenderer,
|
||||||
|
cesiumViewerOf,
|
||||||
|
} from '../render/cesium/appRenderer.js';
|
||||||
import { registerDataCredits } from '../data/dataCredits.js';
|
import { registerDataCredits } from '../data/dataCredits.js';
|
||||||
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
|
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
|
||||||
import { MapStackController } from '../mapStackController.js';
|
import { MapStackController } from '../mapStackController.js';
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
/**
|
/**
|
||||||
* @module app/renderer
|
* @module render/cesium/appRenderer
|
||||||
* @description The application's default renderer plugin: today's Cesium
|
* @description The application's default renderer plugin: today's Cesium
|
||||||
* viewer, set up exactly as before, behind the contract's `RendererPlugin`
|
* viewer, set up exactly as before, behind the contract's `RendererPlugin`
|
||||||
* (`src/render/contract/renderer.d.ts`).
|
* (`src/render/contract/renderer.d.ts`).
|
||||||
*
|
*
|
||||||
* The renderer it makes is transitional: it has only the members it really
|
* The renderer it makes is transitional. Its ports are the shared Cesium
|
||||||
* has, and the contract's ports arrive one layer family at a time (phase 2
|
* ports (`ports.js`), the ones the conformance renderer is checked with,
|
||||||
* of the renderer plan); `checkRendererShape` lists what is still missing.
|
* less the members that would not be true of the application yet: the
|
||||||
|
* globe (it draws photoreal tiles the port does not know), `whenSettled`
|
||||||
|
* (it would not wait for them) and `frame.setTime` (it would stop the
|
||||||
|
* application's clock). Those arrive with the layer families that need
|
||||||
|
* them (phase 2 of the renderer plan); `checkRendererShape` lists what is
|
||||||
|
* still missing.
|
||||||
* Until a family is ported, its code reaches the Cesium viewer through
|
* Until a family is ported, its code reaches the Cesium viewer through
|
||||||
* `cesiumViewerOf`, never through a renderer member, so no engine type
|
* `cesiumViewerOf`, never through a renderer member, so no engine type
|
||||||
* crosses the contract and each call marks code still to port.
|
* crosses the contract and each call marks code still to port.
|
||||||
*/
|
*/
|
||||||
import { CONTRACT_VERSION } from '../render/contract/index.js';
|
import { createApplicationViewer } from '../../app/viewer.js';
|
||||||
import { createApplicationViewer } from './viewer.js';
|
import { CONTRACT_VERSION } from '../contract/index.js';
|
||||||
|
import { createCesiumPorts } from './ports.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The members a transitional renderer has today.
|
* The members a transitional renderer has today.
|
||||||
* @typedef {Pick<import('../render/contract/renderer').Renderer,
|
* @typedef {Pick<import('../contract/renderer').Renderer,
|
||||||
* 'id' | 'contractVersion' | 'capabilities' | 'canvas' | 'resize' | 'destroy'>}
|
* 'id' | 'contractVersion' | 'capabilities' | 'canvas' | 'frame' | 'draw'
|
||||||
|
* | 'camera' | 'query' | 'capture' | 'resize' | 'destroy'>}
|
||||||
* TransitionalRenderer
|
* TransitionalRenderer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -75,22 +82,31 @@ export function createCesiumRendererPlugin({
|
|||||||
creditContainer.id = 'cesium-credits';
|
creditContainer.id = 'cesium-credits';
|
||||||
document.body.appendChild(creditContainer);
|
document.body.appendChild(creditContainer);
|
||||||
let viewer;
|
let viewer;
|
||||||
|
let ports;
|
||||||
try {
|
try {
|
||||||
viewer = createViewer({ container, creditContainer });
|
viewer = createViewer({ container, creditContainer });
|
||||||
|
ports = createCesiumPorts(viewer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (viewer && !viewer.isDestroyed()) viewer.destroy();
|
||||||
creditContainer.remove();
|
creditContainer.remove();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const renderer = Object.freeze({
|
const renderer = Object.freeze({
|
||||||
id: 'cesium',
|
id: 'cesium',
|
||||||
contractVersion: CONTRACT_VERSION,
|
contractVersion: CONTRACT_VERSION,
|
||||||
capabilities: new ReadonlySet(),
|
capabilities: new ReadonlySet(['models']),
|
||||||
canvas: viewer.canvas,
|
canvas: viewer.canvas,
|
||||||
|
frame: ports.frame,
|
||||||
|
draw: ports.draw,
|
||||||
|
camera: ports.camera,
|
||||||
|
query: ports.query,
|
||||||
|
capture: ports.capture,
|
||||||
resize() {
|
resize() {
|
||||||
viewer.resize();
|
viewer.resize();
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
try {
|
try {
|
||||||
|
ports.dispose();
|
||||||
if (!viewer.isDestroyed()) viewer.destroy();
|
if (!viewer.isDestroyed()) viewer.destroy();
|
||||||
} finally {
|
} finally {
|
||||||
creditContainer.remove();
|
creditContainer.remove();
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import {
|
import * as Cesium from 'cesium';
|
||||||
CONTRACT_VERSION,
|
import { CONTRACT_VERSION, checkRendererShape } from '../contract/index.js';
|
||||||
checkRendererShape,
|
|
||||||
} from '../render/contract/index.js';
|
|
||||||
import {
|
import {
|
||||||
ReadonlySet,
|
ReadonlySet,
|
||||||
cesiumRenderer,
|
cesiumRenderer,
|
||||||
cesiumViewerOf,
|
cesiumViewerOf,
|
||||||
createCesiumRendererPlugin,
|
createCesiumRendererPlugin,
|
||||||
} from './renderer.js';
|
} from './appRenderer.js';
|
||||||
|
|
||||||
/** A document with just a body, recording what is attached. */
|
/** A document with just a body, recording what is attached. */
|
||||||
function fakeDocument() {
|
function fakeDocument() {
|
||||||
@@ -32,9 +30,29 @@ function fakeDocument() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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 } = {}) {
|
function fakeViewer(log, { failDestroy = false } = {}) {
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
|
const listeners = new Set();
|
||||||
return {
|
return {
|
||||||
|
listeners,
|
||||||
|
scene: {
|
||||||
|
camera: {},
|
||||||
|
primitives: { add() {}, remove() {} },
|
||||||
|
requestRender() {},
|
||||||
|
preUpdate: fakeEvent(listeners),
|
||||||
|
preRender: fakeEvent(listeners),
|
||||||
|
postRender: fakeEvent(listeners),
|
||||||
|
},
|
||||||
canvas: { tag: 'canvas' },
|
canvas: { tag: 'canvas' },
|
||||||
resize: () => log.push('resize'),
|
resize: () => log.push('resize'),
|
||||||
isDestroyed: () => destroyed,
|
isDestroyed: () => destroyed,
|
||||||
@@ -64,10 +82,12 @@ test('the plugin wraps the viewer it is given, behind the accessor only', async
|
|||||||
withDocument(t, doc);
|
withDocument(t, doc);
|
||||||
const log = [];
|
const log = [];
|
||||||
let given;
|
let given;
|
||||||
|
let viewer;
|
||||||
const plugin = createCesiumRendererPlugin({
|
const plugin = createCesiumRendererPlugin({
|
||||||
createViewer(options) {
|
createViewer(options) {
|
||||||
given = options;
|
given = options;
|
||||||
return fakeViewer(log);
|
viewer = fakeViewer(log);
|
||||||
|
return viewer;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const container = { tag: 'container' };
|
const container = { tag: 'container' };
|
||||||
@@ -94,14 +114,65 @@ test('the plugin wraps the viewer it is given, behind the accessor only', async
|
|||||||
assert.ok(!('viewer' in renderer), 'no engine object is a member');
|
assert.ok(!('viewer' in renderer), 'no engine object is a member');
|
||||||
assert.equal(cesiumViewerOf(renderer).canvas, renderer.canvas);
|
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.resize();
|
||||||
renderer.destroy();
|
renderer.destroy();
|
||||||
|
assert.equal(viewer.listeners.size, 0, 'and stop on destroy');
|
||||||
assert.deepEqual(log, ['resize', 'destroy']);
|
assert.deepEqual(log, ['resize', 'destroy']);
|
||||||
assert.deepEqual(doc.children, [], 'credits leave with the renderer');
|
assert.deepEqual(doc.children, [], 'credits leave with the renderer');
|
||||||
renderer.destroy();
|
renderer.destroy();
|
||||||
assert.deepEqual(log, ['resize', 'destroy'], 'destroying twice is safe');
|
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) => {
|
test('the credits leave even when the viewer fails to destroy', async (t) => {
|
||||||
const doc = fakeDocument();
|
const doc = fakeDocument();
|
||||||
withDocument(t, doc);
|
withDocument(t, doc);
|
||||||
@@ -151,7 +222,7 @@ test('an aborted creation builds nothing', async (t) => {
|
|||||||
assert.deepEqual(doc.children, []);
|
assert.deepEqual(doc.children, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the transitional renderer lacks exactly the ports still to come', async (t) => {
|
test('the transitional renderer lacks exactly the members still to come', async (t) => {
|
||||||
withDocument(t, fakeDocument());
|
withDocument(t, fakeDocument());
|
||||||
const renderer = await createCesiumRendererPlugin({
|
const renderer = await createCesiumRendererPlugin({
|
||||||
createViewer: () => fakeViewer([]),
|
createViewer: () => fakeViewer([]),
|
||||||
@@ -174,9 +245,9 @@ test('only renderers this plugin made reach a Cesium viewer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('the application refuses a renderer it cannot run on, and destroys it', async (t) => {
|
test('the application refuses a renderer it cannot run on, and destroys it', async (t) => {
|
||||||
const { createApplicationScene } = await import('./scene.js');
|
const { createApplicationScene } = await import('../../app/scene.js');
|
||||||
const { createApplicationRequestServices } =
|
const { createApplicationRequestServices } =
|
||||||
await import('../services/requests.js');
|
await import('../../services/requests.js');
|
||||||
const destroyed = [];
|
const destroyed = [];
|
||||||
const deferred = [];
|
const deferred = [];
|
||||||
const plugin = {
|
const plugin = {
|
||||||
@@ -218,18 +289,69 @@ test('the application refuses a renderer it cannot run on, and destroys it', asy
|
|||||||
});
|
});
|
||||||
|
|
||||||
const EXPECTED_MISSING = [
|
const EXPECTED_MISSING = [
|
||||||
'camera',
|
'camera.cancelFlight',
|
||||||
'capture',
|
'camera.controls',
|
||||||
'credits',
|
'camera.controls.coast',
|
||||||
'draw',
|
'camera.controls.coast.drag',
|
||||||
'effects',
|
'camera.controls.coast.zoom',
|
||||||
'frame',
|
'camera.controls.collision',
|
||||||
'globe',
|
'camera.controls.enableRotate',
|
||||||
'input',
|
'camera.controls.enableTilt',
|
||||||
'on',
|
'camera.controls.enableZoom',
|
||||||
'quality',
|
'camera.controls.enabled',
|
||||||
'query',
|
'camera.controls.maxDistance',
|
||||||
'readPixels',
|
'camera.controls.minDistance',
|
||||||
'releaseUnused',
|
'camera.controls.pitchRange',
|
||||||
'whenSettled',
|
'camera.controls.suspend',
|
||||||
].map((member) => `renderer.${member} is missing`);
|
'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`);
|
||||||
@@ -0,0 +1,575 @@
|
|||||||
|
/**
|
||||||
|
* @module render/cesium/ports
|
||||||
|
* @description The Cesium renderer's contract ports, built over a Cesium
|
||||||
|
* widget or viewer someone else made and configured: frame phases, the
|
||||||
|
* draw sets, `setView` and the pose, picks and projection, and capture.
|
||||||
|
* Building them changes nothing in the scene; only calls do.
|
||||||
|
*
|
||||||
|
* The conformance renderer (`sceneRenderer.js`) and the application's
|
||||||
|
* renderer (`appRenderer.js`) both use them, so what conformance
|
||||||
|
* checks is what the application runs. Members a port does not have are
|
||||||
|
* absent, never stubbed.
|
||||||
|
*/
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
|
||||||
|
/** The contract's colour as Cesium's. */
|
||||||
|
export function toColor(color, alpha = 1) {
|
||||||
|
if (color === undefined || color === null)
|
||||||
|
return Cesium.Color.WHITE.withAlpha(alpha);
|
||||||
|
const c =
|
||||||
|
typeof color === 'string'
|
||||||
|
? Cesium.Color.fromCssColorString(color)
|
||||||
|
: new Cesium.Color(color[0], color[1], color[2], color[3] ?? 1);
|
||||||
|
return c.withAlpha(c.alpha * alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An image reference as something a billboard takes. */
|
||||||
|
export function toImage(image) {
|
||||||
|
if (typeof image === 'string') return image;
|
||||||
|
if (image?.svg)
|
||||||
|
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(image.svg)}`;
|
||||||
|
if (image?.canvas) return image.canvas;
|
||||||
|
throw new Error('the Cesium renderer takes URL, SVG and canvas images only');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A contract position as ECEF. */
|
||||||
|
export function toCartesian(pos) {
|
||||||
|
if ('x' in pos) return new Cesium.Cartesian3(pos.x, pos.y, pos.z);
|
||||||
|
return Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.height ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANCHORS = {
|
||||||
|
center: Cesium.VerticalOrigin.CENTER,
|
||||||
|
bottom: Cesium.VerticalOrigin.BOTTOM,
|
||||||
|
top: Cesium.VerticalOrigin.TOP,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} widget A Cesium `CesiumWidget` or `Viewer`.
|
||||||
|
*/
|
||||||
|
export function createCesiumPorts(widget) {
|
||||||
|
const scene = widget.scene;
|
||||||
|
const camera = scene.camera;
|
||||||
|
const sets = new Map();
|
||||||
|
|
||||||
|
const frameListeners = { preUpdate: [], preRender: [], postRender: [] };
|
||||||
|
let frames = 0;
|
||||||
|
const onPhase = (phase) => (_scene, time) => {
|
||||||
|
if (frameListeners[phase].length === 0) return;
|
||||||
|
const t = {
|
||||||
|
frame: frames,
|
||||||
|
time: Cesium.JulianDate.toDate(
|
||||||
|
time ?? widget.clock.currentTime,
|
||||||
|
).getTime(),
|
||||||
|
now: performance.now(),
|
||||||
|
dt: 1 / 60,
|
||||||
|
rendering: phase !== 'preUpdate',
|
||||||
|
};
|
||||||
|
for (const cb of frameListeners[phase].slice()) cb(t);
|
||||||
|
};
|
||||||
|
let disposed = false;
|
||||||
|
const removers = [
|
||||||
|
scene.preUpdate.addEventListener(() => (frames += 1)),
|
||||||
|
scene.preUpdate.addEventListener(onPhase('preUpdate')),
|
||||||
|
scene.preRender.addEventListener(onPhase('preRender')),
|
||||||
|
scene.postRender.addEventListener(onPhase('postRender')),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Pending one-shot renders; disposal settles them rather than leave them
|
||||||
|
// attached to a scene that will not render again.
|
||||||
|
const pendingRenders = new Set();
|
||||||
|
function nextPostRender() {
|
||||||
|
if (disposed) return Promise.resolve();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const done = () => {
|
||||||
|
off();
|
||||||
|
pendingRenders.delete(done);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const off = scene.postRender.addEventListener(done);
|
||||||
|
pendingRenders.add(done);
|
||||||
|
scene.requestRender();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame = {
|
||||||
|
on(phase, cb) {
|
||||||
|
frameListeners[phase].push(cb);
|
||||||
|
return () => {
|
||||||
|
const i = frameListeners[phase].indexOf(cb);
|
||||||
|
if (i >= 0) frameListeners[phase].splice(i, 1);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
requestRender: () => scene.requestRender(),
|
||||||
|
renderNow: () => scene.render(widget.clock.currentTime),
|
||||||
|
renderOnce: () => nextPostRender(),
|
||||||
|
time: () => Cesium.JulianDate.toDate(widget.clock.currentTime).getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Draw ------------------------------------------------------------------
|
||||||
|
|
||||||
|
function makeSet(kind, spec, build) {
|
||||||
|
if (sets.has(spec.id)) throw new Error(`a set with id ${spec.id} is live`);
|
||||||
|
const items = new Map();
|
||||||
|
const listeners = { ready: [] };
|
||||||
|
const record = { kind, spec, items, listeners, host: null, rebuild: null };
|
||||||
|
const handle = {
|
||||||
|
id: spec.id,
|
||||||
|
set(list) {
|
||||||
|
items.clear();
|
||||||
|
for (const item of list) items.set(item.id, item);
|
||||||
|
build(record);
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
upsert(list) {
|
||||||
|
for (const item of list) items.set(item.id, item);
|
||||||
|
build(record);
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
remove(ids) {
|
||||||
|
for (const id of ids) items.delete(id);
|
||||||
|
build(record);
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
items.clear();
|
||||||
|
build(record);
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
setOptions(options) {
|
||||||
|
Object.assign(spec, options);
|
||||||
|
if (record.host) record.host.show = spec.show !== false;
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
size: () => items.size,
|
||||||
|
get show() {
|
||||||
|
return spec.show !== false;
|
||||||
|
},
|
||||||
|
set show(value) {
|
||||||
|
spec.show = value;
|
||||||
|
if (record.host) record.host.show = value;
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
on(event, cb) {
|
||||||
|
listeners[event]?.push(cb);
|
||||||
|
return () => {
|
||||||
|
const list = listeners[event];
|
||||||
|
const i = list?.indexOf(cb) ?? -1;
|
||||||
|
if (i >= 0) list.splice(i, 1);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dispose() {
|
||||||
|
if (record.host && !record.host.isDestroyed?.())
|
||||||
|
scene.primitives.remove(record.host);
|
||||||
|
record.host = null;
|
||||||
|
// A newer set under the same id is not this handle's to remove.
|
||||||
|
if (sets.get(spec.id) === record) sets.delete(spec.id);
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
sets.set(spec.id, record);
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickId = (record, item) => ({
|
||||||
|
set: record.spec.id,
|
||||||
|
id: item.id,
|
||||||
|
meta: item.meta,
|
||||||
|
});
|
||||||
|
|
||||||
|
function replaceHost(record, host) {
|
||||||
|
if (record.host) scene.primitives.remove(record.host);
|
||||||
|
record.host = host;
|
||||||
|
host.show = record.spec.show !== false;
|
||||||
|
scene.primitives.add(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIcons(record) {
|
||||||
|
const host = new Cesium.BillboardCollection({ scene });
|
||||||
|
for (const item of record.items.values()) {
|
||||||
|
const position = toCartesian(item.pos);
|
||||||
|
const options = {
|
||||||
|
position,
|
||||||
|
image: toImage(item.image),
|
||||||
|
width: item.size[0] * (item.scale ?? 1),
|
||||||
|
height: item.size[1] * (item.scale ?? 1),
|
||||||
|
color: toColor(item.color, item.alpha ?? 1),
|
||||||
|
verticalOrigin: ANCHORS[item.anchor ?? 'center'],
|
||||||
|
disableDepthTestDistance: item.disableDepthTest ?? 0,
|
||||||
|
show: item.show !== false,
|
||||||
|
id: pickId(record, item),
|
||||||
|
};
|
||||||
|
if (item.rotationMode === 'screen') options.rotation = item.rotation ?? 0;
|
||||||
|
if (item.rotationMode === 'course') {
|
||||||
|
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(position);
|
||||||
|
options.alignedAxis = Cesium.Matrix4.multiplyByPointAsVector(
|
||||||
|
enu,
|
||||||
|
Cesium.Cartesian3.UNIT_Y,
|
||||||
|
new Cesium.Cartesian3(),
|
||||||
|
);
|
||||||
|
options.rotation = -Cesium.Math.toRadians(item.course ?? 0);
|
||||||
|
}
|
||||||
|
host.add(options);
|
||||||
|
}
|
||||||
|
replaceHost(record, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPoints(record) {
|
||||||
|
const host = new Cesium.PointPrimitiveCollection();
|
||||||
|
for (const item of record.items.values())
|
||||||
|
host.add({
|
||||||
|
position: toCartesian(item.pos),
|
||||||
|
pixelSize: item.pixelSize,
|
||||||
|
color: toColor(item.color),
|
||||||
|
outlineColor: toColor(item.outlineColor ?? [0, 0, 0, 0]),
|
||||||
|
outlineWidth: item.outlineWidth ?? 0,
|
||||||
|
disableDepthTestDistance: item.disableDepthTest ?? 0,
|
||||||
|
show: item.show !== false,
|
||||||
|
id: pickId(record, item),
|
||||||
|
});
|
||||||
|
replaceHost(record, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineMaterial(style) {
|
||||||
|
switch (style.kind) {
|
||||||
|
case 'solid':
|
||||||
|
return Cesium.Material.fromType('Color', {
|
||||||
|
color: toColor(style.color),
|
||||||
|
});
|
||||||
|
case 'glow':
|
||||||
|
return Cesium.Material.fromType('PolylineGlow', {
|
||||||
|
color: toColor(style.color),
|
||||||
|
glowPower: style.power,
|
||||||
|
taperPower: style.taper ?? 1,
|
||||||
|
});
|
||||||
|
case 'dash':
|
||||||
|
return Cesium.Material.fromType('PolylineDash', {
|
||||||
|
color: toColor(style.color),
|
||||||
|
gapColor: toColor(style.gapColor ?? [0, 0, 0, 0]),
|
||||||
|
dashLength: style.length,
|
||||||
|
...(style.pattern16 !== undefined
|
||||||
|
? { dashPattern: style.pattern16 }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
case 'arrow':
|
||||||
|
return Cesium.Material.fromType('PolylineArrow', {
|
||||||
|
color: toColor(style.color),
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw new Error(`the Cesium renderer has no ${style.kind} lines`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function linePositions(item) {
|
||||||
|
const flat = item.positions instanceof Float64Array;
|
||||||
|
const points = [];
|
||||||
|
const n = flat ? item.positions.length / 3 : item.positions.length;
|
||||||
|
for (let i = 0; i < n; i++)
|
||||||
|
points.push(
|
||||||
|
flat
|
||||||
|
? new Cesium.Cartesian3(
|
||||||
|
item.positions[i * 3],
|
||||||
|
item.positions[i * 3 + 1],
|
||||||
|
item.positions[i * 3 + 2],
|
||||||
|
)
|
||||||
|
: toCartesian(item.positions[i]),
|
||||||
|
);
|
||||||
|
if (item.mode !== 'geodesic') return points;
|
||||||
|
const dense = [];
|
||||||
|
for (let i = 0; i + 1 < points.length; i++) {
|
||||||
|
const a = Cesium.Cartographic.fromCartesian(points[i]);
|
||||||
|
const b = Cesium.Cartographic.fromCartesian(points[i + 1]);
|
||||||
|
const geodesic = new Cesium.EllipsoidGeodesic(a, b);
|
||||||
|
for (let k = 0; k < 64; k++) {
|
||||||
|
const s = k / 64;
|
||||||
|
const at = geodesic.interpolateUsingFraction(s);
|
||||||
|
dense.push(
|
||||||
|
Cesium.Cartesian3.fromRadians(
|
||||||
|
at.longitude,
|
||||||
|
at.latitude,
|
||||||
|
a.height + (b.height - a.height) * s,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dense.push(points.at(-1));
|
||||||
|
return dense;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLines(record) {
|
||||||
|
const host = new Cesium.PolylineCollection();
|
||||||
|
for (const item of record.items.values()) {
|
||||||
|
if (item.mode === 'draped')
|
||||||
|
throw new Error('the Cesium renderer has no draped lines');
|
||||||
|
host.add({
|
||||||
|
positions: linePositions(item),
|
||||||
|
width: item.widthPx,
|
||||||
|
material: lineMaterial(item.style),
|
||||||
|
show: item.show !== false,
|
||||||
|
id: pickId(record, item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
replaceHost(record, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
const heightOf = (pos) =>
|
||||||
|
'x' in pos
|
||||||
|
? Cesium.Cartographic.fromCartesian(toCartesian(pos)).height
|
||||||
|
: (pos.height ?? 0);
|
||||||
|
|
||||||
|
function shapeGeometry(item) {
|
||||||
|
const g = item.geometry;
|
||||||
|
const extrudedHeight = item.extrudeHeight;
|
||||||
|
switch (g.kind) {
|
||||||
|
case 'polygon': {
|
||||||
|
const [outer, ...holes] = g.rings.map((ring) => ring.map(toCartesian));
|
||||||
|
const hierarchy = new Cesium.PolygonHierarchy(
|
||||||
|
outer,
|
||||||
|
holes.map((h) => new Cesium.PolygonHierarchy(h)),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
fill: new Cesium.PolygonGeometry({
|
||||||
|
polygonHierarchy: hierarchy,
|
||||||
|
height: g.perPositionHeight ? undefined : heightOf(g.rings[0][0]),
|
||||||
|
extrudedHeight,
|
||||||
|
perPositionHeight: g.perPositionHeight,
|
||||||
|
}),
|
||||||
|
ring: outer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'ellipse':
|
||||||
|
return {
|
||||||
|
fill: new Cesium.EllipseGeometry({
|
||||||
|
center: toCartesian(g.center),
|
||||||
|
semiMajorAxis: g.semiMajor,
|
||||||
|
semiMinorAxis: g.semiMinor,
|
||||||
|
rotation: g.rotation ?? 0,
|
||||||
|
height: heightOf(g.center),
|
||||||
|
extrudedHeight,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
case 'rectangle':
|
||||||
|
return {
|
||||||
|
fill: new Cesium.RectangleGeometry({
|
||||||
|
rectangle: Cesium.Rectangle.fromDegrees(
|
||||||
|
g.rect.west,
|
||||||
|
g.rect.south,
|
||||||
|
g.rect.east,
|
||||||
|
g.rect.north,
|
||||||
|
),
|
||||||
|
height: g.height ?? 0,
|
||||||
|
extrudedHeight,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
case 'box': {
|
||||||
|
const d = g.dimensions;
|
||||||
|
return {
|
||||||
|
fill: Cesium.BoxGeometry.fromDimensions({
|
||||||
|
dimensions: new Cesium.Cartesian3(d.x, d.y, d.z),
|
||||||
|
}),
|
||||||
|
modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(
|
||||||
|
toCartesian(g.center),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`the Cesium renderer has no ${g.kind} shapes`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildShapes(record) {
|
||||||
|
const host = new Cesium.PrimitiveCollection();
|
||||||
|
const outlines = new Cesium.PolylineCollection();
|
||||||
|
for (const item of record.items.values()) {
|
||||||
|
if (item.show === false) continue;
|
||||||
|
if (
|
||||||
|
item.fill &&
|
||||||
|
typeof item.fill === 'object' &&
|
||||||
|
!Array.isArray(item.fill)
|
||||||
|
)
|
||||||
|
throw new Error('the Cesium renderer fills shapes with colours only');
|
||||||
|
const shape = shapeGeometry(item);
|
||||||
|
const id = pickId(record, item);
|
||||||
|
const solid =
|
||||||
|
item.extrudeHeight !== undefined || item.geometry.kind === 'box';
|
||||||
|
if (item.fill && !item.outlineOnly) {
|
||||||
|
const color = toColor(item.fill);
|
||||||
|
host.add(
|
||||||
|
new Cesium.Primitive({
|
||||||
|
geometryInstances: new Cesium.GeometryInstance({
|
||||||
|
geometry: shape.fill,
|
||||||
|
modelMatrix: shape.modelMatrix,
|
||||||
|
attributes: {
|
||||||
|
color: Cesium.ColorGeometryInstanceAttribute.fromColor(color),
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
}),
|
||||||
|
appearance: new Cesium.PerInstanceColorAppearance({
|
||||||
|
translucent: color.alpha < 1,
|
||||||
|
closed: solid,
|
||||||
|
// Flat shapes unlit: lit, their two triangles shade apart.
|
||||||
|
flat: !solid,
|
||||||
|
}),
|
||||||
|
asynchronous: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (item.outline && shape.ring)
|
||||||
|
outlines.add({
|
||||||
|
positions: [...shape.ring, shape.ring[0]],
|
||||||
|
width: item.outlineWidth ?? 1,
|
||||||
|
material: Cesium.Material.fromType('Color', {
|
||||||
|
color: toColor(item.outline),
|
||||||
|
}),
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
host.add(outlines);
|
||||||
|
replaceHost(record, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModels(record) {
|
||||||
|
const host = new Cesium.PrimitiveCollection();
|
||||||
|
replaceHost(record, host);
|
||||||
|
// A model counts as loading until it is ready or fails, not when added;
|
||||||
|
// each model settles once, and a failure is kept for `whenSettled`.
|
||||||
|
host.loading = record.items.size;
|
||||||
|
host.failed = 0;
|
||||||
|
for (const item of record.items.values()) {
|
||||||
|
let settled = false;
|
||||||
|
const settle = (failed) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
host.loading -= 1;
|
||||||
|
if (failed) host.failed += 1;
|
||||||
|
scene.requestRender();
|
||||||
|
};
|
||||||
|
const o = item.orientation;
|
||||||
|
const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
|
||||||
|
toCartesian(item.pos),
|
||||||
|
new Cesium.HeadingPitchRoll(o.heading, o.pitch, o.roll),
|
||||||
|
);
|
||||||
|
Cesium.Model.fromGltfAsync({
|
||||||
|
url: item.url,
|
||||||
|
modelMatrix,
|
||||||
|
scale: item.scale ?? 1,
|
||||||
|
minimumPixelSize: item.minPixelSize ?? 0,
|
||||||
|
// All textures before ready, so a settled capture is the whole model.
|
||||||
|
incrementallyLoadTextures: false,
|
||||||
|
id: pickId(record, item),
|
||||||
|
}).then(
|
||||||
|
(model) => {
|
||||||
|
// Rebuilt, disposed, or the renderer is gone.
|
||||||
|
if (record.host !== host || host.isDestroyed())
|
||||||
|
return model.destroy();
|
||||||
|
host.add(model);
|
||||||
|
model.readyEvent.addEventListener(() => {
|
||||||
|
settle(false);
|
||||||
|
for (const cb of record.listeners.ready.slice()) cb(item.id);
|
||||||
|
});
|
||||||
|
model.errorEvent.addEventListener(() => settle(true));
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
() => settle(true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const draw = {
|
||||||
|
icons: (spec) => makeSet('icons', { ...spec }, buildIcons),
|
||||||
|
points: (spec) => makeSet('points', { ...spec }, buildPoints),
|
||||||
|
lines: (spec) => makeSet('lines', { ...spec }, buildLines),
|
||||||
|
shapes: (spec) => makeSet('shapes', { ...spec }, buildShapes),
|
||||||
|
models: (spec) => makeSet('models', { ...spec }, buildModels),
|
||||||
|
};
|
||||||
|
|
||||||
|
const cameraPort = {
|
||||||
|
setView(spec) {
|
||||||
|
const d = spec.destination;
|
||||||
|
if (d && 'rect' in d)
|
||||||
|
throw new Error('the Cesium renderer takes position destinations only');
|
||||||
|
const o = spec.orientation ?? {};
|
||||||
|
camera.setView({
|
||||||
|
...(d ? { destination: toCartesian(d) } : {}),
|
||||||
|
orientation: {
|
||||||
|
heading: o.heading ?? 0,
|
||||||
|
pitch: o.pitch ?? -Math.PI / 2,
|
||||||
|
roll: o.roll ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
pose() {
|
||||||
|
const v = (c) => ({ x: c.x, y: c.y, z: c.z });
|
||||||
|
return {
|
||||||
|
position: v(camera.positionWC),
|
||||||
|
direction: v(camera.directionWC),
|
||||||
|
up: v(camera.upWC),
|
||||||
|
right: v(camera.rightWC),
|
||||||
|
heading: camera.heading,
|
||||||
|
pitch: camera.pitch,
|
||||||
|
roll: camera.roll,
|
||||||
|
height: camera.positionCartographic.height,
|
||||||
|
fovy: camera.frustum.fovy,
|
||||||
|
aspectRatio: camera.frustum.aspectRatio,
|
||||||
|
near: camera.frustum.near,
|
||||||
|
far: camera.frustum.far,
|
||||||
|
frameIsIdentity: Cesium.Matrix4.equals(
|
||||||
|
camera.transform,
|
||||||
|
Cesium.Matrix4.IDENTITY,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
pick(x, y) {
|
||||||
|
const picked = scene.pick(new Cesium.Cartesian2(x, y));
|
||||||
|
const id = picked?.id;
|
||||||
|
return id && typeof id.set === 'string'
|
||||||
|
? { ...id, surface: false }
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
project(position) {
|
||||||
|
const s = Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||||
|
scene,
|
||||||
|
toCartesian(position),
|
||||||
|
);
|
||||||
|
return s ? { x: s.x, y: s.y } : null;
|
||||||
|
},
|
||||||
|
pickEllipsoid(x, y) {
|
||||||
|
const c = camera.pickEllipsoid(new Cesium.Cartesian2(x, y));
|
||||||
|
return c ? { x: c.x, y: c.y, z: c.z } : null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function capture({ type = 'png' } = {}) {
|
||||||
|
scene.render(widget.clock.currentTime);
|
||||||
|
return new Promise((resolve) =>
|
||||||
|
scene.canvas.toBlob(resolve, `image/${type}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The live model sets, for a renderer's own `whenSettled`. */
|
||||||
|
const modelSets = () =>
|
||||||
|
[...sets.values()].filter((r) => r.kind === 'models' && r.host);
|
||||||
|
|
||||||
|
return {
|
||||||
|
frame,
|
||||||
|
draw,
|
||||||
|
camera: cameraPort,
|
||||||
|
query,
|
||||||
|
capture,
|
||||||
|
modelSets,
|
||||||
|
nextPostRender,
|
||||||
|
/** Detach from the scene; the widget's owner destroys the widget. */
|
||||||
|
dispose() {
|
||||||
|
disposed = true;
|
||||||
|
for (const off of removers) off();
|
||||||
|
for (const done of [...pendingRenders]) done();
|
||||||
|
// Late model loads find no host and destroy themselves.
|
||||||
|
for (const record of sets.values()) record.host = null;
|
||||||
|
sets.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,11 +6,18 @@
|
|||||||
* phases, settling and capture. The `/dev/conformance/` page draws the
|
* phases, settling and capture. The `/dev/conformance/` page draws the
|
||||||
* scenes with it to make and check the visual references.
|
* scenes with it to make and check the visual references.
|
||||||
*
|
*
|
||||||
|
* Its ports are `ports.js`'s, which the application's renderer shares;
|
||||||
|
* this factory adds the conformance widget, its fixed settings and the
|
||||||
|
* bare-globe and sky members.
|
||||||
|
*
|
||||||
* It is not yet the whole Cesium renderer: each later migration adds the
|
* It is not yet the whole Cesium renderer: each later migration adds the
|
||||||
* members its layer family needs, until it passes the whole suite.
|
* members its layer family needs, until it passes the whole suite.
|
||||||
* Members it does not have are absent, never stubbed.
|
* Members it does not have are absent, never stubbed.
|
||||||
*/
|
*/
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { createCesiumPorts } from './ports.js';
|
||||||
|
|
||||||
|
export { toCartesian, toColor, toImage } from './ports.js';
|
||||||
|
|
||||||
const SKY_DEFAULTS = Object.freeze({
|
const SKY_DEFAULTS = Object.freeze({
|
||||||
atmosphere: false,
|
atmosphere: false,
|
||||||
@@ -22,38 +29,6 @@ const SKY_DEFAULTS = Object.freeze({
|
|||||||
lighting: false,
|
lighting: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** The contract's colour as Cesium's. */
|
|
||||||
export function toColor(color, alpha = 1) {
|
|
||||||
if (color === undefined || color === null)
|
|
||||||
return Cesium.Color.WHITE.withAlpha(alpha);
|
|
||||||
const c =
|
|
||||||
typeof color === 'string'
|
|
||||||
? Cesium.Color.fromCssColorString(color)
|
|
||||||
: new Cesium.Color(color[0], color[1], color[2], color[3] ?? 1);
|
|
||||||
return c.withAlpha(c.alpha * alpha);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An image reference as something a billboard takes. */
|
|
||||||
export function toImage(image) {
|
|
||||||
if (typeof image === 'string') return image;
|
|
||||||
if (image?.svg)
|
|
||||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(image.svg)}`;
|
|
||||||
if (image?.canvas) return image.canvas;
|
|
||||||
throw new Error('the scene renderer takes URL, SVG and canvas images only');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A contract position as ECEF. */
|
|
||||||
export function toCartesian(pos) {
|
|
||||||
if ('x' in pos) return new Cesium.Cartesian3(pos.x, pos.y, pos.z);
|
|
||||||
return Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.height ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ANCHORS = {
|
|
||||||
center: Cesium.VerticalOrigin.CENTER,
|
|
||||||
bottom: Cesium.VerticalOrigin.BOTTOM,
|
|
||||||
top: Cesium.VerticalOrigin.TOP,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{container: HTMLElement, msaa?: number}} options
|
* @param {{container: HTMLElement, msaa?: number}} options
|
||||||
* @returns {Promise<object>} A renderer with the members listed above.
|
* @returns {Promise<object>} A renderer with the members listed above.
|
||||||
@@ -72,7 +47,6 @@ export async function createCesiumSceneRenderer({ container, msaa = 4 }) {
|
|||||||
contextOptions: { webgl: { preserveDrawingBuffer: true } },
|
contextOptions: { webgl: { preserveDrawingBuffer: true } },
|
||||||
});
|
});
|
||||||
const scene = widget.scene;
|
const scene = widget.scene;
|
||||||
const camera = scene.camera;
|
|
||||||
// A fixed time, so lighting is the same in every capture.
|
// A fixed time, so lighting is the same in every capture.
|
||||||
widget.clock.shouldAnimate = false;
|
widget.clock.shouldAnimate = false;
|
||||||
widget.clock.currentTime = Cesium.JulianDate.fromIso8601(
|
widget.clock.currentTime = Cesium.JulianDate.fromIso8601(
|
||||||
@@ -85,7 +59,6 @@ export async function createCesiumSceneRenderer({ container, msaa = 4 }) {
|
|||||||
if (scene.sun) scene.sun.show = false;
|
if (scene.sun) scene.sun.show = false;
|
||||||
if (scene.moon) scene.moon.show = false;
|
if (scene.moon) scene.moon.show = false;
|
||||||
|
|
||||||
const sets = new Map();
|
|
||||||
const sky = { ...SKY_DEFAULTS };
|
const sky = { ...SKY_DEFAULTS };
|
||||||
const capabilities = new Set(['models']);
|
const capabilities = new Set(['models']);
|
||||||
|
|
||||||
@@ -98,49 +71,9 @@ export async function createCesiumSceneRenderer({ container, msaa = 4 }) {
|
|||||||
scene.requestRender();
|
scene.requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
const frameListeners = { preUpdate: [], preRender: [], postRender: [] };
|
const ports = createCesiumPorts(widget);
|
||||||
let frames = 0;
|
|
||||||
const onPhase = (phase) => (_scene, time) => {
|
|
||||||
const t = {
|
|
||||||
frame: frames,
|
|
||||||
time: Cesium.JulianDate.toDate(
|
|
||||||
time ?? widget.clock.currentTime,
|
|
||||||
).getTime(),
|
|
||||||
now: performance.now(),
|
|
||||||
dt: 1 / 60,
|
|
||||||
rendering: phase !== 'preUpdate',
|
|
||||||
};
|
|
||||||
for (const cb of frameListeners[phase].slice()) cb(t);
|
|
||||||
};
|
|
||||||
const removers = [
|
|
||||||
scene.preUpdate.addEventListener(() => (frames += 1)),
|
|
||||||
scene.preUpdate.addEventListener(onPhase('preUpdate')),
|
|
||||||
scene.preRender.addEventListener(onPhase('preRender')),
|
|
||||||
scene.postRender.addEventListener(onPhase('postRender')),
|
|
||||||
];
|
|
||||||
|
|
||||||
function nextPostRender() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const off = scene.postRender.addEventListener(() => {
|
|
||||||
off();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
scene.requestRender();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const frame = {
|
const frame = {
|
||||||
on(phase, cb) {
|
...ports.frame,
|
||||||
frameListeners[phase].push(cb);
|
|
||||||
return () => {
|
|
||||||
const i = frameListeners[phase].indexOf(cb);
|
|
||||||
if (i >= 0) frameListeners[phase].splice(i, 1);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
requestRender: () => scene.requestRender(),
|
|
||||||
renderNow: () => scene.render(widget.clock.currentTime),
|
|
||||||
renderOnce: () => nextPostRender(),
|
|
||||||
time: () => Cesium.JulianDate.toDate(widget.clock.currentTime).getTime(),
|
|
||||||
setTime(ms) {
|
setTime(ms) {
|
||||||
widget.clock.currentTime = Cesium.JulianDate.fromDate(new Date(ms));
|
widget.clock.currentTime = Cesium.JulianDate.fromDate(new Date(ms));
|
||||||
widget.clock.shouldAnimate = false;
|
widget.clock.shouldAnimate = false;
|
||||||
@@ -148,382 +81,7 @@ export async function createCesiumSceneRenderer({ container, msaa = 4 }) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Draw ------------------------------------------------------------------
|
// --- Globe -----------------------------------------------------------------
|
||||||
|
|
||||||
function makeSet(kind, spec, build) {
|
|
||||||
if (sets.has(spec.id)) throw new Error(`a set with id ${spec.id} is live`);
|
|
||||||
const items = new Map();
|
|
||||||
const listeners = { ready: [] };
|
|
||||||
const record = { kind, spec, items, listeners, host: null, rebuild: null };
|
|
||||||
const handle = {
|
|
||||||
id: spec.id,
|
|
||||||
set(list) {
|
|
||||||
items.clear();
|
|
||||||
for (const item of list) items.set(item.id, item);
|
|
||||||
build(record);
|
|
||||||
scene.requestRender();
|
|
||||||
},
|
|
||||||
upsert(list) {
|
|
||||||
for (const item of list) items.set(item.id, item);
|
|
||||||
build(record);
|
|
||||||
scene.requestRender();
|
|
||||||
},
|
|
||||||
remove(ids) {
|
|
||||||
for (const id of ids) items.delete(id);
|
|
||||||
build(record);
|
|
||||||
scene.requestRender();
|
|
||||||
},
|
|
||||||
clear() {
|
|
||||||
items.clear();
|
|
||||||
build(record);
|
|
||||||
scene.requestRender();
|
|
||||||
},
|
|
||||||
setOptions(options) {
|
|
||||||
Object.assign(spec, options);
|
|
||||||
if (record.host) record.host.show = spec.show !== false;
|
|
||||||
scene.requestRender();
|
|
||||||
},
|
|
||||||
size: () => items.size,
|
|
||||||
get show() {
|
|
||||||
return spec.show !== false;
|
|
||||||
},
|
|
||||||
set show(value) {
|
|
||||||
spec.show = value;
|
|
||||||
if (record.host) record.host.show = value;
|
|
||||||
scene.requestRender();
|
|
||||||
},
|
|
||||||
on(event, cb) {
|
|
||||||
listeners[event]?.push(cb);
|
|
||||||
return () => {
|
|
||||||
const list = listeners[event];
|
|
||||||
const i = list?.indexOf(cb) ?? -1;
|
|
||||||
if (i >= 0) list.splice(i, 1);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
dispose() {
|
|
||||||
if (record.host && !record.host.isDestroyed?.())
|
|
||||||
scene.primitives.remove(record.host);
|
|
||||||
record.host = null;
|
|
||||||
// A newer set under the same id is not this handle's to remove.
|
|
||||||
if (sets.get(spec.id) === record) sets.delete(spec.id);
|
|
||||||
scene.requestRender();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
sets.set(spec.id, record);
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pickId = (record, item) => ({
|
|
||||||
set: record.spec.id,
|
|
||||||
id: item.id,
|
|
||||||
meta: item.meta,
|
|
||||||
});
|
|
||||||
|
|
||||||
function replaceHost(record, host) {
|
|
||||||
if (record.host) scene.primitives.remove(record.host);
|
|
||||||
record.host = host;
|
|
||||||
host.show = record.spec.show !== false;
|
|
||||||
scene.primitives.add(host);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildIcons(record) {
|
|
||||||
const host = new Cesium.BillboardCollection({ scene });
|
|
||||||
for (const item of record.items.values()) {
|
|
||||||
const position = toCartesian(item.pos);
|
|
||||||
const options = {
|
|
||||||
position,
|
|
||||||
image: toImage(item.image),
|
|
||||||
width: item.size[0] * (item.scale ?? 1),
|
|
||||||
height: item.size[1] * (item.scale ?? 1),
|
|
||||||
color: toColor(item.color, item.alpha ?? 1),
|
|
||||||
verticalOrigin: ANCHORS[item.anchor ?? 'center'],
|
|
||||||
disableDepthTestDistance: item.disableDepthTest ?? 0,
|
|
||||||
show: item.show !== false,
|
|
||||||
id: pickId(record, item),
|
|
||||||
};
|
|
||||||
if (item.rotationMode === 'screen') options.rotation = item.rotation ?? 0;
|
|
||||||
if (item.rotationMode === 'course') {
|
|
||||||
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(position);
|
|
||||||
options.alignedAxis = Cesium.Matrix4.multiplyByPointAsVector(
|
|
||||||
enu,
|
|
||||||
Cesium.Cartesian3.UNIT_Y,
|
|
||||||
new Cesium.Cartesian3(),
|
|
||||||
);
|
|
||||||
options.rotation = -Cesium.Math.toRadians(item.course ?? 0);
|
|
||||||
}
|
|
||||||
host.add(options);
|
|
||||||
}
|
|
||||||
replaceHost(record, host);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPoints(record) {
|
|
||||||
const host = new Cesium.PointPrimitiveCollection();
|
|
||||||
for (const item of record.items.values())
|
|
||||||
host.add({
|
|
||||||
position: toCartesian(item.pos),
|
|
||||||
pixelSize: item.pixelSize,
|
|
||||||
color: toColor(item.color),
|
|
||||||
outlineColor: toColor(item.outlineColor ?? [0, 0, 0, 0]),
|
|
||||||
outlineWidth: item.outlineWidth ?? 0,
|
|
||||||
disableDepthTestDistance: item.disableDepthTest ?? 0,
|
|
||||||
show: item.show !== false,
|
|
||||||
id: pickId(record, item),
|
|
||||||
});
|
|
||||||
replaceHost(record, host);
|
|
||||||
}
|
|
||||||
|
|
||||||
function lineMaterial(style) {
|
|
||||||
switch (style.kind) {
|
|
||||||
case 'solid':
|
|
||||||
return Cesium.Material.fromType('Color', {
|
|
||||||
color: toColor(style.color),
|
|
||||||
});
|
|
||||||
case 'glow':
|
|
||||||
return Cesium.Material.fromType('PolylineGlow', {
|
|
||||||
color: toColor(style.color),
|
|
||||||
glowPower: style.power,
|
|
||||||
taperPower: style.taper ?? 1,
|
|
||||||
});
|
|
||||||
case 'dash':
|
|
||||||
return Cesium.Material.fromType('PolylineDash', {
|
|
||||||
color: toColor(style.color),
|
|
||||||
gapColor: toColor(style.gapColor ?? [0, 0, 0, 0]),
|
|
||||||
dashLength: style.length,
|
|
||||||
...(style.pattern16 !== undefined
|
|
||||||
? { dashPattern: style.pattern16 }
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
case 'arrow':
|
|
||||||
return Cesium.Material.fromType('PolylineArrow', {
|
|
||||||
color: toColor(style.color),
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
throw new Error(`the scene renderer has no ${style.kind} lines`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function linePositions(item) {
|
|
||||||
const flat = item.positions instanceof Float64Array;
|
|
||||||
const points = [];
|
|
||||||
const n = flat ? item.positions.length / 3 : item.positions.length;
|
|
||||||
for (let i = 0; i < n; i++)
|
|
||||||
points.push(
|
|
||||||
flat
|
|
||||||
? new Cesium.Cartesian3(
|
|
||||||
item.positions[i * 3],
|
|
||||||
item.positions[i * 3 + 1],
|
|
||||||
item.positions[i * 3 + 2],
|
|
||||||
)
|
|
||||||
: toCartesian(item.positions[i]),
|
|
||||||
);
|
|
||||||
if (item.mode !== 'geodesic') return points;
|
|
||||||
const dense = [];
|
|
||||||
for (let i = 0; i + 1 < points.length; i++) {
|
|
||||||
const a = Cesium.Cartographic.fromCartesian(points[i]);
|
|
||||||
const b = Cesium.Cartographic.fromCartesian(points[i + 1]);
|
|
||||||
const geodesic = new Cesium.EllipsoidGeodesic(a, b);
|
|
||||||
for (let k = 0; k < 64; k++) {
|
|
||||||
const s = k / 64;
|
|
||||||
const at = geodesic.interpolateUsingFraction(s);
|
|
||||||
dense.push(
|
|
||||||
Cesium.Cartesian3.fromRadians(
|
|
||||||
at.longitude,
|
|
||||||
at.latitude,
|
|
||||||
a.height + (b.height - a.height) * s,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dense.push(points.at(-1));
|
|
||||||
return dense;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildLines(record) {
|
|
||||||
const host = new Cesium.PolylineCollection();
|
|
||||||
for (const item of record.items.values()) {
|
|
||||||
if (item.mode === 'draped')
|
|
||||||
throw new Error('the scene renderer has no draped lines');
|
|
||||||
host.add({
|
|
||||||
positions: linePositions(item),
|
|
||||||
width: item.widthPx,
|
|
||||||
material: lineMaterial(item.style),
|
|
||||||
show: item.show !== false,
|
|
||||||
id: pickId(record, item),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
replaceHost(record, host);
|
|
||||||
}
|
|
||||||
|
|
||||||
const heightOf = (pos) =>
|
|
||||||
'x' in pos
|
|
||||||
? Cesium.Cartographic.fromCartesian(toCartesian(pos)).height
|
|
||||||
: (pos.height ?? 0);
|
|
||||||
|
|
||||||
function shapeGeometry(item) {
|
|
||||||
const g = item.geometry;
|
|
||||||
const extrudedHeight = item.extrudeHeight;
|
|
||||||
switch (g.kind) {
|
|
||||||
case 'polygon': {
|
|
||||||
const [outer, ...holes] = g.rings.map((ring) => ring.map(toCartesian));
|
|
||||||
const hierarchy = new Cesium.PolygonHierarchy(
|
|
||||||
outer,
|
|
||||||
holes.map((h) => new Cesium.PolygonHierarchy(h)),
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
fill: new Cesium.PolygonGeometry({
|
|
||||||
polygonHierarchy: hierarchy,
|
|
||||||
height: g.perPositionHeight ? undefined : heightOf(g.rings[0][0]),
|
|
||||||
extrudedHeight,
|
|
||||||
perPositionHeight: g.perPositionHeight,
|
|
||||||
}),
|
|
||||||
ring: outer,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'ellipse':
|
|
||||||
return {
|
|
||||||
fill: new Cesium.EllipseGeometry({
|
|
||||||
center: toCartesian(g.center),
|
|
||||||
semiMajorAxis: g.semiMajor,
|
|
||||||
semiMinorAxis: g.semiMinor,
|
|
||||||
rotation: g.rotation ?? 0,
|
|
||||||
height: heightOf(g.center),
|
|
||||||
extrudedHeight,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
case 'rectangle':
|
|
||||||
return {
|
|
||||||
fill: new Cesium.RectangleGeometry({
|
|
||||||
rectangle: Cesium.Rectangle.fromDegrees(
|
|
||||||
g.rect.west,
|
|
||||||
g.rect.south,
|
|
||||||
g.rect.east,
|
|
||||||
g.rect.north,
|
|
||||||
),
|
|
||||||
height: g.height ?? 0,
|
|
||||||
extrudedHeight,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
case 'box': {
|
|
||||||
const d = g.dimensions;
|
|
||||||
return {
|
|
||||||
fill: Cesium.BoxGeometry.fromDimensions({
|
|
||||||
dimensions: new Cesium.Cartesian3(d.x, d.y, d.z),
|
|
||||||
}),
|
|
||||||
modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(
|
|
||||||
toCartesian(g.center),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(`the scene renderer has no ${g.kind} shapes`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildShapes(record) {
|
|
||||||
const host = new Cesium.PrimitiveCollection();
|
|
||||||
const outlines = new Cesium.PolylineCollection();
|
|
||||||
for (const item of record.items.values()) {
|
|
||||||
if (item.show === false) continue;
|
|
||||||
if (
|
|
||||||
item.fill &&
|
|
||||||
typeof item.fill === 'object' &&
|
|
||||||
!Array.isArray(item.fill)
|
|
||||||
)
|
|
||||||
throw new Error('the scene renderer fills shapes with colours only');
|
|
||||||
const shape = shapeGeometry(item);
|
|
||||||
const id = pickId(record, item);
|
|
||||||
const solid =
|
|
||||||
item.extrudeHeight !== undefined || item.geometry.kind === 'box';
|
|
||||||
if (item.fill && !item.outlineOnly) {
|
|
||||||
const color = toColor(item.fill);
|
|
||||||
host.add(
|
|
||||||
new Cesium.Primitive({
|
|
||||||
geometryInstances: new Cesium.GeometryInstance({
|
|
||||||
geometry: shape.fill,
|
|
||||||
modelMatrix: shape.modelMatrix,
|
|
||||||
attributes: {
|
|
||||||
color: Cesium.ColorGeometryInstanceAttribute.fromColor(color),
|
|
||||||
},
|
|
||||||
id,
|
|
||||||
}),
|
|
||||||
appearance: new Cesium.PerInstanceColorAppearance({
|
|
||||||
translucent: color.alpha < 1,
|
|
||||||
closed: solid,
|
|
||||||
// Flat shapes unlit: lit, their two triangles shade apart.
|
|
||||||
flat: !solid,
|
|
||||||
}),
|
|
||||||
asynchronous: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (item.outline && shape.ring)
|
|
||||||
outlines.add({
|
|
||||||
positions: [...shape.ring, shape.ring[0]],
|
|
||||||
width: item.outlineWidth ?? 1,
|
|
||||||
material: Cesium.Material.fromType('Color', {
|
|
||||||
color: toColor(item.outline),
|
|
||||||
}),
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
host.add(outlines);
|
|
||||||
replaceHost(record, host);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildModels(record) {
|
|
||||||
const host = new Cesium.PrimitiveCollection();
|
|
||||||
replaceHost(record, host);
|
|
||||||
// A model counts as loading until it is ready or fails, not when added;
|
|
||||||
// each model settles once, and a failure is kept for `whenSettled`.
|
|
||||||
host.loading = record.items.size;
|
|
||||||
host.failed = 0;
|
|
||||||
for (const item of record.items.values()) {
|
|
||||||
let settled = false;
|
|
||||||
const settle = (failed) => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
host.loading -= 1;
|
|
||||||
if (failed) host.failed += 1;
|
|
||||||
scene.requestRender();
|
|
||||||
};
|
|
||||||
const o = item.orientation;
|
|
||||||
const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
|
|
||||||
toCartesian(item.pos),
|
|
||||||
new Cesium.HeadingPitchRoll(o.heading, o.pitch, o.roll),
|
|
||||||
);
|
|
||||||
Cesium.Model.fromGltfAsync({
|
|
||||||
url: item.url,
|
|
||||||
modelMatrix,
|
|
||||||
scale: item.scale ?? 1,
|
|
||||||
minimumPixelSize: item.minPixelSize ?? 0,
|
|
||||||
// All textures before ready, so a settled capture is the whole model.
|
|
||||||
incrementallyLoadTextures: false,
|
|
||||||
id: pickId(record, item),
|
|
||||||
}).then(
|
|
||||||
(model) => {
|
|
||||||
if (record.host !== host) return model.destroy();
|
|
||||||
host.add(model);
|
|
||||||
model.readyEvent.addEventListener(() => {
|
|
||||||
settle(false);
|
|
||||||
for (const cb of record.listeners.ready.slice()) cb(item.id);
|
|
||||||
});
|
|
||||||
model.errorEvent.addEventListener(() => settle(true));
|
|
||||||
scene.requestRender();
|
|
||||||
},
|
|
||||||
() => settle(true),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const draw = {
|
|
||||||
icons: (spec) => makeSet('icons', { ...spec }, buildIcons),
|
|
||||||
points: (spec) => makeSet('points', { ...spec }, buildPoints),
|
|
||||||
lines: (spec) => makeSet('lines', { ...spec }, buildLines),
|
|
||||||
shapes: (spec) => makeSet('shapes', { ...spec }, buildShapes),
|
|
||||||
models: (spec) => makeSet('models', { ...spec }, buildModels),
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Globe, camera, query --------------------------------------------------
|
|
||||||
|
|
||||||
const globe = {
|
const globe = {
|
||||||
async setBase(base) {
|
async setBase(base) {
|
||||||
@@ -549,75 +107,14 @@ export async function createCesiumSceneRenderer({ container, msaa = 4 }) {
|
|||||||
isSettled: () => scene.globe.tilesLoaded,
|
isSettled: () => scene.globe.tilesLoaded,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cameraPort = {
|
|
||||||
setView(spec) {
|
|
||||||
const d = spec.destination;
|
|
||||||
if (d && 'rect' in d)
|
|
||||||
throw new Error('the scene renderer takes position destinations only');
|
|
||||||
const o = spec.orientation ?? {};
|
|
||||||
camera.setView({
|
|
||||||
...(d ? { destination: toCartesian(d) } : {}),
|
|
||||||
orientation: {
|
|
||||||
heading: o.heading ?? 0,
|
|
||||||
pitch: o.pitch ?? -Math.PI / 2,
|
|
||||||
roll: o.roll ?? 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
scene.requestRender();
|
|
||||||
},
|
|
||||||
pose() {
|
|
||||||
const v = (c) => ({ x: c.x, y: c.y, z: c.z });
|
|
||||||
return {
|
|
||||||
position: v(camera.positionWC),
|
|
||||||
direction: v(camera.directionWC),
|
|
||||||
up: v(camera.upWC),
|
|
||||||
right: v(camera.rightWC),
|
|
||||||
heading: camera.heading,
|
|
||||||
pitch: camera.pitch,
|
|
||||||
roll: camera.roll,
|
|
||||||
height: camera.positionCartographic.height,
|
|
||||||
fovy: camera.frustum.fovy,
|
|
||||||
aspectRatio: camera.frustum.aspectRatio,
|
|
||||||
near: camera.frustum.near,
|
|
||||||
far: camera.frustum.far,
|
|
||||||
frameIsIdentity: Cesium.Matrix4.equals(
|
|
||||||
camera.transform,
|
|
||||||
Cesium.Matrix4.IDENTITY,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
pick(x, y) {
|
|
||||||
const picked = scene.pick(new Cesium.Cartesian2(x, y));
|
|
||||||
const id = picked?.id;
|
|
||||||
return id && typeof id.set === 'string'
|
|
||||||
? { ...id, surface: false }
|
|
||||||
: null;
|
|
||||||
},
|
|
||||||
project(position) {
|
|
||||||
const s = Cesium.SceneTransforms.worldToWindowCoordinates(
|
|
||||||
scene,
|
|
||||||
toCartesian(position),
|
|
||||||
);
|
|
||||||
return s ? { x: s.x, y: s.y } : null;
|
|
||||||
},
|
|
||||||
pickEllipsoid(x, y) {
|
|
||||||
const c = camera.pickEllipsoid(new Cesium.Cartesian2(x, y));
|
|
||||||
return c ? { x: c.x, y: c.y, z: c.z } : null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Renderer --------------------------------------------------------------
|
// --- Renderer --------------------------------------------------------------
|
||||||
|
|
||||||
async function whenSettled({ timeoutMs = 20000 } = {}) {
|
async function whenSettled({ timeoutMs = 20000 } = {}) {
|
||||||
const models = () =>
|
const models = ports.modelSets;
|
||||||
[...sets.values()].filter((r) => r.kind === 'models' && r.host);
|
|
||||||
const end = performance.now() + timeoutMs;
|
const end = performance.now() + timeoutMs;
|
||||||
let quiet = 0;
|
let quiet = 0;
|
||||||
while (performance.now() < end) {
|
while (performance.now() < end) {
|
||||||
await nextPostRender();
|
await ports.nextPostRender();
|
||||||
if (models().some((r) => r.host.failed > 0))
|
if (models().some((r) => r.host.failed > 0))
|
||||||
return { state: 'failed', blockers: ['models'] };
|
return { state: 'failed', blockers: ['models'] };
|
||||||
const loading = models().some((r) => r.host.loading > 0);
|
const loading = models().some((r) => r.host.loading > 0);
|
||||||
@@ -637,19 +134,14 @@ export async function createCesiumSceneRenderer({ container, msaa = 4 }) {
|
|||||||
canvas: scene.canvas,
|
canvas: scene.canvas,
|
||||||
frame,
|
frame,
|
||||||
globe,
|
globe,
|
||||||
draw,
|
draw: ports.draw,
|
||||||
camera: cameraPort,
|
camera: ports.camera,
|
||||||
query,
|
query: ports.query,
|
||||||
async capture({ type = 'png' } = {}) {
|
capture: ports.capture,
|
||||||
scene.render(widget.clock.currentTime);
|
|
||||||
return new Promise((resolve) =>
|
|
||||||
scene.canvas.toBlob(resolve, `image/${type}`),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
whenSettled,
|
whenSettled,
|
||||||
resize: () => widget.resize(),
|
resize: () => widget.resize(),
|
||||||
destroy() {
|
destroy() {
|
||||||
for (const off of removers) off();
|
ports.dispose();
|
||||||
widget.destroy();
|
widget.destroy();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user