mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 05:42:22 +00:00
Settle imagery, world data and sky capabilities in the renderer contract
- setFrames takes a timed source (URL frames, or video tiles in segments) on the scene clock by default or its own; frameReady fires once each time the shown frame is drawn. - drapeOn is 'surface' | 'objects' | 'all' plus lit; imagery.host() and onHostChange go, and the drape rule Core derives is written out. - ImagerySpec.fetchTile answers bytes, missing or defer on every renderer; tileTransform, admit and canvas imagery need imageHooks, and add() throws for them without it. - Generic names for the default renderer's world data: worldDataToken, the photoreal fallback route, and world terrain and imagery. - clouds and water capabilities, with globe.setClouds and setWater.
This commit is contained in:
1 parent
e9c0dc5405
commit
fefdf242ee
7 files changed
+360
-52
No files matched your search
@@ -166,4 +166,6 @@ easing (`courseBlendMs`, `courseGlide`), and a hidden row keeping its eased
|
|||||||
course. The stub keeps no drawn course. These wait for scenes that capture
|
course. The stub keeps no drawn course. These wait for scenes that capture
|
||||||
an icon mid-turn. Height queries that count a set
|
an icon mid-turn. Height queries that count a set
|
||||||
(`includeInHeightQueries`) are not checked either: the stub's heights are
|
(`includeInHeightQueries`) are not checked either: the stub's heights are
|
||||||
scripted, so it does not model them.
|
scripted, so it does not model them. Nor are video frames (a check needs a
|
||||||
|
video), `fetchTile` (the stub fetches nothing) or what clouds and water
|
||||||
|
draw.
|
||||||
@@ -20,6 +20,14 @@ import {
|
|||||||
} from 'satellite.js';
|
} from 'satellite.js';
|
||||||
import { deepEqual, equal, near, nearVec, ok, throws } from '../expect.js';
|
import { deepEqual, equal, near, nearVec, ok, throws } from '../expect.js';
|
||||||
|
|
||||||
|
/** 1 × 1 PNGs (red, green, blue), so imagery checks load nothing from the network. */
|
||||||
|
const PIXEL_PNGS = [
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==',
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNg+M/wHwAEAQH/cetH5QAAAABJRU5ErkJggg==',
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYPj/HwADAgH/5ncLrgAAAABJRU5ErkJggg==',
|
||||||
|
];
|
||||||
|
const PIXEL_PNG = PIXEL_PNGS[0];
|
||||||
|
|
||||||
/** Elements for the orbit check: a station-like orbit, epoch 2026-01-04. */
|
/** Elements for the orbit check: a station-like orbit, epoch 2026-01-04. */
|
||||||
const ISS_TLE = Object.freeze([
|
const ISS_TLE = Object.freeze([
|
||||||
'1 25544U 98067A 26004.50000000 .00016717 00000-0 10270-3 0 9992',
|
'1 25544U 98067A 26004.50000000 .00016717 00000-0 10270-3 0 9992',
|
||||||
@@ -1674,11 +1682,161 @@ export function portChecks() {
|
|||||||
deepEqual(ours(), ['a', 'b'], 'lowered');
|
deepEqual(ours(), ['a', 'b'], 'lowered');
|
||||||
a.alpha = 0.5;
|
a.alpha = 0.5;
|
||||||
equal(a.alpha, 0.5, 'alpha');
|
equal(a.alpha, 0.5, 'alpha');
|
||||||
|
equal(b.drapeOn, 'all', 'drapes on whatever is drawn by default');
|
||||||
|
equal(b.lit, true, 'lit by default');
|
||||||
|
b.drapeOn = 'surface';
|
||||||
|
b.lit = false;
|
||||||
|
equal(b.drapeOn, 'surface', 'drapeOn is settable');
|
||||||
|
equal(b.lit, false, 'lit is settable');
|
||||||
a.remove();
|
a.remove();
|
||||||
deepEqual(ours(), ['b'], 'the removed layer is the one gone');
|
deepEqual(ours(), ['b'], 'the removed layer is the one gone');
|
||||||
b.remove();
|
b.remove();
|
||||||
deepEqual(ours(), [], 'both removed');
|
deepEqual(ours(), [], 'both removed');
|
||||||
ok(['globe', 'tiles', 'both'].includes(r.globe.imagery.host()), 'host');
|
},
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'globe.imagery.frames',
|
||||||
|
'timed frames follow scene time, or setFrameTime on their own clock',
|
||||||
|
async (h, r) => {
|
||||||
|
await overAustin(h, r, 2000000);
|
||||||
|
const t0 = r.frame.time();
|
||||||
|
const frames = [0, 60000, 120000].map((dt, i) => ({
|
||||||
|
time: t0 + dt,
|
||||||
|
url: PIXEL_PNGS[i],
|
||||||
|
}));
|
||||||
|
const ready = [];
|
||||||
|
// Frames decode in real time: wait on the clock, drawing meanwhile.
|
||||||
|
const waitFor = async (time, ms = 5000) => {
|
||||||
|
const until = Date.now() + ms;
|
||||||
|
while (!ready.includes(time) && Date.now() < until) {
|
||||||
|
await drawn(h, r, 1);
|
||||||
|
await new Promise((done) => setTimeout(done, 10));
|
||||||
|
}
|
||||||
|
return ready.includes(time);
|
||||||
|
};
|
||||||
|
const settle = () => waitFor(Symbol('nothing'), 300);
|
||||||
|
const scene = r.globe.imagery.add({
|
||||||
|
kind: 'single-image',
|
||||||
|
url: PIXEL_PNG,
|
||||||
|
});
|
||||||
|
scene.on('frameReady', (time) => ready.push(time));
|
||||||
|
scene.setFrames({ kind: 'frames', frames });
|
||||||
|
r.frame.setTime(t0 + 50000);
|
||||||
|
ok(await waitFor(t0 + 60000), 'the frame nearest scene time is ready');
|
||||||
|
await settle();
|
||||||
|
ready.length = 0;
|
||||||
|
r.frame.setTime(t0 + 55000);
|
||||||
|
scene.setFrameTime(t0 + 130000);
|
||||||
|
await settle();
|
||||||
|
deepEqual(
|
||||||
|
ready,
|
||||||
|
[],
|
||||||
|
'the same frame is not ready twice, and setFrameTime does nothing on the scene clock',
|
||||||
|
);
|
||||||
|
scene.remove();
|
||||||
|
|
||||||
|
ready.length = 0;
|
||||||
|
const own = r.globe.imagery.add({
|
||||||
|
kind: 'single-image',
|
||||||
|
url: PIXEL_PNG,
|
||||||
|
});
|
||||||
|
own.on('frameReady', (time) => ready.push(time));
|
||||||
|
own.setFrames({ kind: 'frames', frames }, { clock: 'own' });
|
||||||
|
own.setFrameTime(t0 + 130000);
|
||||||
|
ok(await waitFor(t0 + 120000), 'setFrameTime on its own clock');
|
||||||
|
await settle();
|
||||||
|
ready.length = 0;
|
||||||
|
r.frame.setTime(t0 + 5000);
|
||||||
|
await settle();
|
||||||
|
deepEqual(ready, [], 'scene time does not move its own clock');
|
||||||
|
own.remove();
|
||||||
|
|
||||||
|
ready.length = 0;
|
||||||
|
const paused = r.globe.imagery.add({
|
||||||
|
kind: 'single-image',
|
||||||
|
url: PIXEL_PNG,
|
||||||
|
});
|
||||||
|
paused.on('frameReady', (time) => ready.push(time));
|
||||||
|
r.frame.suspend(true);
|
||||||
|
paused.setFrames({ kind: 'frames', frames });
|
||||||
|
r.frame.setTime(t0 + 1000);
|
||||||
|
r.frame.setTime(t0 + 61000);
|
||||||
|
await settle();
|
||||||
|
deepEqual(ready, [], 'nothing is ready while the loop is suspended');
|
||||||
|
r.frame.suspend(false);
|
||||||
|
ok(
|
||||||
|
await waitFor(t0 + 60000),
|
||||||
|
'the frame shown is ready once it is drawn',
|
||||||
|
);
|
||||||
|
equal(ready[0], t0 + 60000, 'and frames never drawn are not announced');
|
||||||
|
paused.remove();
|
||||||
|
|
||||||
|
ready.length = 0;
|
||||||
|
const first = r.globe.imagery.add({
|
||||||
|
kind: 'single-image',
|
||||||
|
url: PIXEL_PNG,
|
||||||
|
});
|
||||||
|
const second = r.globe.imagery.add({
|
||||||
|
kind: 'single-image',
|
||||||
|
url: PIXEL_PNG,
|
||||||
|
});
|
||||||
|
first.on('frameReady', () => {
|
||||||
|
ready.push('first');
|
||||||
|
second.remove();
|
||||||
|
});
|
||||||
|
second.on('frameReady', () => ready.push('second'));
|
||||||
|
first.setFrames({ kind: 'frames', frames });
|
||||||
|
second.setFrames({ kind: 'frames', frames });
|
||||||
|
r.frame.setTime(t0 + 120000);
|
||||||
|
await waitFor('first');
|
||||||
|
await settle();
|
||||||
|
ok(
|
||||||
|
!ready.includes('second'),
|
||||||
|
'a layer removed while another announces says nothing',
|
||||||
|
);
|
||||||
|
first.remove();
|
||||||
|
},
|
||||||
|
{ capability: 'imageryFrames' },
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'globe.withoutImageHooks',
|
||||||
|
'without imageHooks, a spec with main-thread tile hooks throws',
|
||||||
|
async (h) => {
|
||||||
|
const r = await h.create({
|
||||||
|
viewport: SCRIPT_VIEWPORT,
|
||||||
|
capabilities: CAPABILITIES.filter((c) => c !== 'imageHooks'),
|
||||||
|
});
|
||||||
|
if (!r) return { skip: 'this harness cannot drop capabilities' };
|
||||||
|
try {
|
||||||
|
const url = 'https://example.invalid/{z}/{x}/{y}.png';
|
||||||
|
throws(
|
||||||
|
() =>
|
||||||
|
r.globe.imagery.add({
|
||||||
|
kind: 'xyz',
|
||||||
|
url,
|
||||||
|
tileTransform: (t, i) => i,
|
||||||
|
}),
|
||||||
|
'tileTransform',
|
||||||
|
);
|
||||||
|
throws(
|
||||||
|
() =>
|
||||||
|
r.globe.imagery.add({
|
||||||
|
kind: 'xyz',
|
||||||
|
url,
|
||||||
|
admit: (t, issue) => issue(),
|
||||||
|
}),
|
||||||
|
'admit',
|
||||||
|
);
|
||||||
|
throws(
|
||||||
|
() => r.globe.imagery.add({ kind: 'canvas', drawTile: () => {} }),
|
||||||
|
'canvas imagery',
|
||||||
|
);
|
||||||
|
const plain = r.globe.imagery.add({ kind: 'xyz', url });
|
||||||
|
ok(plain, 'a plain layer is added');
|
||||||
|
plain.remove();
|
||||||
|
} finally {
|
||||||
|
r.destroy();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
check(
|
check(
|
||||||
|
|||||||
Vendored
+6
@@ -160,6 +160,12 @@ export type Capability =
|
|||||||
| 'imageryFrames'
|
| 'imageryFrames'
|
||||||
/** Wind-relief and similar shading of the globe surface. */
|
/** Wind-relief and similar shading of the globe surface. */
|
||||||
| 'surfaceShading'
|
| 'surfaceShading'
|
||||||
|
/** Volumetric clouds (`globe.setClouds`). */
|
||||||
|
| 'clouds'
|
||||||
|
/** A water surface on the oceans (`globe.setWater`). */
|
||||||
|
| 'water'
|
||||||
|
/** Main-thread tile hooks: `tileTransform`, `admit` and `canvas` imagery. */
|
||||||
|
| 'imageHooks'
|
||||||
/** A screen-space sector modulation pass (a sweeping sonar band). */
|
/** A screen-space sector modulation pass (a sweeping sonar band). */
|
||||||
| 'screenModulation'
|
| 'screenModulation'
|
||||||
/** GPU frame timing through timer queries. */
|
/** GPU frame timing through timer queries. */
|
||||||
|
|||||||
Vendored
+95
-18
@@ -10,13 +10,16 @@ export interface PhotorealSpec {
|
|||||||
key?: string;
|
key?: string;
|
||||||
/** Renews a session token; called again when the token expires. */
|
/** Renews a session token; called again when the token expires. */
|
||||||
token?: () => Promise<string>;
|
token?: () => Promise<string>;
|
||||||
/** Fall back to an ion-hosted copy when Google refuses. */
|
/**
|
||||||
ionFallback?: boolean;
|
* Fall back to the renderer's copy of the same tiles when Google refuses
|
||||||
|
* (for the default renderer, one hosted on Cesium ion).
|
||||||
|
*/
|
||||||
|
fallback?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PhotorealStatus {
|
export interface PhotorealStatus {
|
||||||
/** Which route is drawing, or `'none'` when photoreal is off or failed. */
|
/** Which route is drawing, or `'none'` when photoreal is off or failed. */
|
||||||
route: 'google' | 'ion' | 'none';
|
route: 'google' | 'fallback' | 'none';
|
||||||
error?: { code: string; message: string };
|
error?: { code: string; message: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +32,11 @@ export interface PhotorealStatus {
|
|||||||
export type TerrainSpec = (
|
export type TerrainSpec = (
|
||||||
| { kind: 'ellipsoid' }
|
| { kind: 'ellipsoid' }
|
||||||
| { kind: 'quantizedMesh'; url: string; requestVertexNormals?: boolean }
|
| { kind: 'quantizedMesh'; url: string; requestVertexNormals?: boolean }
|
||||||
| { kind: 'ion'; assetId: number }
|
/**
|
||||||
|
* The renderer's global terrain, through `CreateOptions.worldDataToken`
|
||||||
|
* (for the default renderer, Cesium World Terrain).
|
||||||
|
*/
|
||||||
|
| { kind: 'world'; requestVertexNormals?: boolean }
|
||||||
/** A DEM image pyramid (`{z}/{x}/{y}`), heights encoded in RGB. */
|
/** A DEM image pyramid (`{z}/{x}/{y}`), heights encoded in RGB. */
|
||||||
| {
|
| {
|
||||||
kind: 'heightmap';
|
kind: 'heightmap';
|
||||||
@@ -53,11 +60,27 @@ export interface TileRequest {
|
|||||||
level: number;
|
level: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What a tile fetch answered: the bytes to decode, `missing` (no tile
|
||||||
|
* there; draw the parent's), or `defer` (ask again on a later frame).
|
||||||
|
*/
|
||||||
|
export type TileFetch =
|
||||||
|
| { bytes: ArrayBuffer; contentType?: string }
|
||||||
|
| { missing: true }
|
||||||
|
| { defer: true };
|
||||||
|
|
||||||
export interface ImagerySpec {
|
export interface ImagerySpec {
|
||||||
kind: 'xyz' | 'wmts' | 'arcgis' | 'bing' | 'single-image' | 'canvas';
|
/**
|
||||||
|
* `world`: the renderer's global imagery through
|
||||||
|
* `CreateOptions.worldDataToken` (for the default renderer, Cesium ion's
|
||||||
|
* world imagery), in `worldStyle`. `canvas`: `imageHooks`.
|
||||||
|
*/
|
||||||
|
kind: 'xyz' | 'wmts' | 'arcgis' | 'world' | 'single-image' | 'canvas';
|
||||||
|
/** `world`: aerial (the default), or aerial with labels. */
|
||||||
|
worldStyle?: 'aerial' | 'aerialLabels';
|
||||||
/** URL or URL template (`{x}`, `{y}`, `{z}`, `{s}`, `{time}`). */
|
/** URL or URL template (`{x}`, `{y}`, `{z}`, `{s}`, `{time}`). */
|
||||||
url?: string;
|
url?: string;
|
||||||
/** `'canvas'`: draws one tile. */
|
/** `'canvas'` (`imageHooks`): draws one tile. */
|
||||||
drawTile?: (tile: TileRequest, canvas: HTMLCanvasElement) => void;
|
drawTile?: (tile: TileRequest, canvas: HTMLCanvasElement) => void;
|
||||||
/** WMTS: layer, style, tile matrix set and format. */
|
/** WMTS: layer, style, tile matrix set and format. */
|
||||||
wmts?: {
|
wmts?: {
|
||||||
@@ -77,14 +100,26 @@ export interface ImagerySpec {
|
|||||||
maxConcurrentRequests?: number;
|
maxConcurrentRequests?: number;
|
||||||
/** HTTP statuses retried rather than treated as missing. */
|
/** HTTP statuses retried rather than treated as missing. */
|
||||||
retryStatuses?: number[];
|
retryStatuses?: number[];
|
||||||
/** Changes a tile's image before it is drawn. */
|
/**
|
||||||
|
* Fetches a tile in place of the renderer, with the URL it would fetch.
|
||||||
|
* Every renderer calls it, before its own decoding (in a worker, say).
|
||||||
|
*/
|
||||||
|
fetchTile?: (request: {
|
||||||
|
tile: TileRequest;
|
||||||
|
url: string;
|
||||||
|
signal: AbortSignal;
|
||||||
|
}) => Promise<TileFetch>;
|
||||||
|
/** `imageHooks`: changes a tile's decoded image before it is drawn. */
|
||||||
tileTransform?: (
|
tileTransform?: (
|
||||||
tile: TileRequest,
|
tile: TileRequest,
|
||||||
image: ImageBitmap,
|
image: ImageBitmap,
|
||||||
) => ImageBitmap | Promise<ImageBitmap>;
|
) => ImageBitmap | Promise<ImageBitmap>;
|
||||||
/**
|
/**
|
||||||
* Decides each tile request: call `issue()` to fetch it, or return
|
* `imageHooks`: decides each tile request on the main thread: call
|
||||||
* `undefined` to defer it and be asked again on a later frame.
|
* `issue()` to fetch it, or return `undefined` to defer it and be asked
|
||||||
|
* again on a later frame. `fetchTile`'s `defer` does the same anywhere.
|
||||||
|
* With both, `admit` decides first and an issued request goes through
|
||||||
|
* `fetchTile`; each defers on its own.
|
||||||
*/
|
*/
|
||||||
admit?: (
|
admit?: (
|
||||||
tile: TileRequest,
|
tile: TileRequest,
|
||||||
@@ -100,20 +135,56 @@ export interface TimedUrl {
|
|||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A timed source: frames as URLs (one template per time), or video tiles
|
||||||
|
* in segments, each a template whose tiles are videos spanning `start` to
|
||||||
|
* `end` (epoch ms), a time within shown by its offset into the segment's
|
||||||
|
* videos. Outside every segment, the nearest segment end is shown.
|
||||||
|
*/
|
||||||
|
export type TimedSource =
|
||||||
|
| { kind: 'frames'; frames: TimedUrl[] }
|
||||||
|
| {
|
||||||
|
kind: 'video';
|
||||||
|
segments: Array<{ url: string; start: number; end: number }>;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ImageryLayer {
|
export interface ImageryLayer {
|
||||||
alpha: number;
|
alpha: number;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
/** Which side of the split this layer shows on. */
|
/** Which side of the split this layer shows on. */
|
||||||
split: 'none' | 'left' | 'right';
|
split: 'none' | 'left' | 'right';
|
||||||
/** What it drapes onto. `'active'` follows whichever base is showing. */
|
/**
|
||||||
drapeOn: 'globe' | 'tiles' | 'both' | 'active';
|
* What it drapes onto: the globe's surface (terrain or the ellipsoid),
|
||||||
/** `imageryFrames`: a timed sequence, prefetched hidden. */
|
* objects (3D tiles and models; `imageryOnTiles`), or all of them. The
|
||||||
|
* default `all` drapes on whichever is drawn, so under photoreal it is
|
||||||
|
* on the tiles. Without `imageryOnTiles`, `objects` drapes on nothing and
|
||||||
|
* `all` on the surface. So nothing drapes while the globe is hidden and
|
||||||
|
* either photoreal's route is `none` or there is no `imageryOnTiles`:
|
||||||
|
* Core reads that from what it set, with no event for it.
|
||||||
|
*/
|
||||||
|
drapeOn: 'surface' | 'objects' | 'all';
|
||||||
|
/** Shaded by the sun like the surface (the default), or drawn unlit. */
|
||||||
|
lit: boolean;
|
||||||
|
/**
|
||||||
|
* `imageryFrames`: a timed source, prefetched hidden. `clock: 'scene'`
|
||||||
|
* (the default) shows the frame for the frame port's scene time;
|
||||||
|
* `'own'` shows the one `setFrameTime` names.
|
||||||
|
*/
|
||||||
setFrames?(
|
setFrames?(
|
||||||
frames: TimedUrl[],
|
source: TimedSource,
|
||||||
options?: { mode?: 'nearest' | 'blend'; prefetch?: number },
|
options?: {
|
||||||
|
mode?: 'nearest' | 'blend';
|
||||||
|
prefetch?: number;
|
||||||
|
clock?: 'scene' | 'own';
|
||||||
|
},
|
||||||
): void;
|
): void;
|
||||||
/** `imageryFrames`: which time to show. */
|
/** `imageryFrames`: which time to show, on the `own` clock. */
|
||||||
setFrameTime?(ms: number): void;
|
setFrameTime?(ms: number): void;
|
||||||
|
/**
|
||||||
|
* `imageryFrames`: the frame the layer's clock now shows (by its time)
|
||||||
|
* has been drawn. Fires once each time the shown frame changes, never
|
||||||
|
* for frames only prefetched.
|
||||||
|
*/
|
||||||
on(event: 'frameReady', cb: (time: number) => void): Off;
|
on(event: 'frameReady', cb: (time: number) => void): Off;
|
||||||
on(
|
on(
|
||||||
event: 'tileError',
|
event: 'tileError',
|
||||||
@@ -189,12 +260,18 @@ export interface GlobePort {
|
|||||||
setGlobeVisible(visible: boolean): void;
|
setGlobeVisible(visible: boolean): void;
|
||||||
/** `surfaceShading`. */
|
/** `surfaceShading`. */
|
||||||
setSurfaceShading?(shading: SurfaceShading | null): void;
|
setSurfaceShading?(shading: SurfaceShading | null): void;
|
||||||
|
/** `clouds`: the renderer's volumetric clouds, or none. */
|
||||||
|
setClouds?(clouds: { coverage: number } | null): void;
|
||||||
|
/** `water`: the renderer's own water surface on the oceans, on or off. */
|
||||||
|
setWater?(enabled: boolean): void;
|
||||||
readonly imagery: {
|
readonly imagery: {
|
||||||
|
/**
|
||||||
|
* Without `imageHooks`, a spec with `tileTransform`, `admit` or kind
|
||||||
|
* `canvas` throws, rather than drawing tiles the hooks were meant to
|
||||||
|
* change.
|
||||||
|
*/
|
||||||
add(spec: ImagerySpec, index?: number): ImageryLayer;
|
add(spec: ImagerySpec, index?: number): ImageryLayer;
|
||||||
list(): ImageryLayer[];
|
list(): ImageryLayer[];
|
||||||
/** What imagery drapes onto now. */
|
|
||||||
host(): 'globe' | 'tiles' | 'both';
|
|
||||||
onHostChange(cb: (host: 'globe' | 'tiles' | 'both') => void): Off;
|
|
||||||
/** `splitSwipe`: split position, a fraction of the canvas's CSS width. */
|
/** `splitSwipe`: split position, a fraction of the canvas's CSS width. */
|
||||||
getSplit?(): number;
|
getSplit?(): number;
|
||||||
/** `splitSwipe`: move the split to `x01` of the canvas's CSS width. */
|
/** `splitSwipe`: move the split to `x01` of the canvas's CSS width. */
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export const CAPABILITIES = Object.freeze([
|
|||||||
'splitSwipe',
|
'splitSwipe',
|
||||||
'imageryFrames',
|
'imageryFrames',
|
||||||
'surfaceShading',
|
'surfaceShading',
|
||||||
|
'clouds',
|
||||||
|
'water',
|
||||||
|
'imageHooks',
|
||||||
'screenModulation',
|
'screenModulation',
|
||||||
'gpuTiming',
|
'gpuTiming',
|
||||||
'pickPosition',
|
'pickPosition',
|
||||||
@@ -138,8 +141,6 @@ export const PORTS = deepFreeze({
|
|||||||
'imagery',
|
'imagery',
|
||||||
'imagery.add',
|
'imagery.add',
|
||||||
'imagery.list',
|
'imagery.list',
|
||||||
'imagery.host',
|
|
||||||
'imagery.onHostChange',
|
|
||||||
'sky',
|
'sky',
|
||||||
'sky.get',
|
'sky.get',
|
||||||
'sky.set',
|
'sky.set',
|
||||||
@@ -147,6 +148,8 @@ export const PORTS = deepFreeze({
|
|||||||
'on',
|
'on',
|
||||||
),
|
),
|
||||||
['setSurfaceShading', 'surfaceShading'],
|
['setSurfaceShading', 'surfaceShading'],
|
||||||
|
['setClouds', 'clouds'],
|
||||||
|
['setWater', 'water'],
|
||||||
['imagery.getSplit', 'splitSwipe'],
|
['imagery.getSplit', 'splitSwipe'],
|
||||||
['imagery.setSplit', 'splitSwipe'],
|
['imagery.setSplit', 'splitSwipe'],
|
||||||
]),
|
]),
|
||||||
@@ -293,6 +296,7 @@ export const PORTS = deepFreeze({
|
|||||||
'show',
|
'show',
|
||||||
'split',
|
'split',
|
||||||
'drapeOn',
|
'drapeOn',
|
||||||
|
'lit',
|
||||||
'on',
|
'on',
|
||||||
'raise',
|
'raise',
|
||||||
'lower',
|
'lower',
|
||||||
|
|||||||
Vendored
+6
-2
@@ -34,8 +34,12 @@ export interface CreateOptions {
|
|||||||
googleApiKey?: string;
|
googleApiKey?: string;
|
||||||
/** Short-lived Google session tokens, when the key is held server-side. */
|
/** Short-lived Google session tokens, when the key is held server-side. */
|
||||||
googleTokens?: () => Promise<string>;
|
googleTokens?: () => Promise<string>;
|
||||||
/** Cesium ion token, for ion-hosted terrain and fallback tiles. */
|
/**
|
||||||
ionToken?: string;
|
* The token for the renderer's world data: `world` terrain and imagery
|
||||||
|
* and the photoreal `fallback`. For the default renderer, a Cesium ion
|
||||||
|
* token.
|
||||||
|
*/
|
||||||
|
worldDataToken?: string;
|
||||||
/** Initial multisampling; quality can change it later. */
|
/** Initial multisampling; quality can change it later. */
|
||||||
msaa?: 0 | 2 | 4 | 8;
|
msaa?: 0 | 2 | 4 | 8;
|
||||||
/** Keep the drawing buffer after presenting, for capture. */
|
/** Keep the drawing buffer after presenting, for capture. */
|
||||||
|
|||||||
+85
-28
@@ -201,6 +201,9 @@ export function createStubRenderer(options = {}) {
|
|||||||
runPhase('preRender', t);
|
runPhase('preRender', t);
|
||||||
step('render');
|
step('render');
|
||||||
rendered += 1;
|
rendered += 1;
|
||||||
|
// A frame each timed layer was asked to show is now drawn.
|
||||||
|
for (const [layer, flush] of [...frameFlushes])
|
||||||
|
if (frameFlushes.get(layer) === flush) flush();
|
||||||
step('postRender');
|
step('postRender');
|
||||||
runPhase('postRender', t);
|
runPhase('postRender', t);
|
||||||
for (const waiter of onceWaiters.splice(0)) waiter();
|
for (const waiter of onceWaiters.splice(0)) waiter();
|
||||||
@@ -277,6 +280,7 @@ export function createStubRenderer(options = {}) {
|
|||||||
},
|
},
|
||||||
setTime(ms) {
|
setTime(ms) {
|
||||||
sceneTime = ms;
|
sceneTime = ms;
|
||||||
|
for (const show of [...sceneFrameLayers.values()]) show(ms);
|
||||||
requestRender('time');
|
requestRender('time');
|
||||||
},
|
},
|
||||||
time: () => sceneTime,
|
time: () => sceneTime,
|
||||||
@@ -916,8 +920,10 @@ export function createStubRenderer(options = {}) {
|
|||||||
|
|
||||||
const imageryLayers = [];
|
const imageryLayers = [];
|
||||||
let split = 0.5;
|
let split = 0.5;
|
||||||
let imageryHost = 'globe';
|
/** Layers whose frames follow scene time, each with its show-a-time function. */
|
||||||
const hostListeners = [];
|
const sceneFrameLayers = new Map();
|
||||||
|
/** Each timed layer's announcement of the frame it drew, run after a render. */
|
||||||
|
const frameFlushes = new Map();
|
||||||
const progressListeners = [];
|
const progressListeners = [];
|
||||||
const settled = { base: true, tiles: true, terrain: true, imagery: true };
|
const settled = { base: true, tiles: true, terrain: true, imagery: true };
|
||||||
let sky = {
|
let sky = {
|
||||||
@@ -933,13 +939,21 @@ export function createStubRenderer(options = {}) {
|
|||||||
let photorealStatus = { route: 'none' };
|
let photorealStatus = { route: 'none' };
|
||||||
|
|
||||||
function addImagery(spec, index) {
|
function addImagery(spec, index) {
|
||||||
|
if (
|
||||||
|
!capabilities.has('imageHooks') &&
|
||||||
|
(spec.tileTransform || spec.admit || spec.kind === 'canvas')
|
||||||
|
)
|
||||||
|
throw new TypeError(
|
||||||
|
'tileTransform, admit and canvas imagery need imageHooks',
|
||||||
|
);
|
||||||
const listeners = { frameReady: [], tileError: [] };
|
const listeners = { frameReady: [], tileError: [] };
|
||||||
const layer = {
|
const layer = {
|
||||||
spec: { ...spec },
|
spec: { ...spec },
|
||||||
alpha: 1,
|
alpha: 1,
|
||||||
show: true,
|
show: true,
|
||||||
split: 'none',
|
split: 'none',
|
||||||
drapeOn: 'active',
|
drapeOn: 'all',
|
||||||
|
lit: true,
|
||||||
on(event, cb) {
|
on(event, cb) {
|
||||||
listeners[event]?.push(cb);
|
listeners[event]?.push(cb);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -967,23 +981,67 @@ export function createStubRenderer(options = {}) {
|
|||||||
remove() {
|
remove() {
|
||||||
const i = imageryLayers.indexOf(layer);
|
const i = imageryLayers.indexOf(layer);
|
||||||
if (i >= 0) imageryLayers.splice(i, 1);
|
if (i >= 0) imageryLayers.splice(i, 1);
|
||||||
|
sceneFrameLayers.delete(layer);
|
||||||
|
frameFlushes.delete(layer);
|
||||||
requestRender('imagery');
|
requestRender('imagery');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (capabilities.has('imageryFrames')) {
|
if (capabilities.has('imageryFrames')) {
|
||||||
let frames = [];
|
let source = null;
|
||||||
layer.setFrames = (list) => {
|
let clockKind = 'scene';
|
||||||
frames = list.slice();
|
/** The time the source shows for `ms`: the nearest frame, or within the videos. */
|
||||||
|
const shownTime = (ms) => {
|
||||||
|
if (source?.kind === 'video') {
|
||||||
|
let best = null;
|
||||||
|
let bestOff = Infinity;
|
||||||
|
for (const seg of source.segments) {
|
||||||
|
const t = Math.min(seg.end, Math.max(seg.start, ms));
|
||||||
|
if (Math.abs(t - ms) < bestOff) {
|
||||||
|
best = t;
|
||||||
|
bestOff = Math.abs(t - ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
const frames = source?.frames ?? [];
|
||||||
|
let best = null;
|
||||||
|
for (const f of frames)
|
||||||
|
if (!best || Math.abs(f.time - ms) < Math.abs(best.time - ms))
|
||||||
|
best = f;
|
||||||
|
return best ? best.time : null;
|
||||||
|
};
|
||||||
|
// The stub has every frame at once, so the frame asked for is drawn
|
||||||
|
// by the next render; it is said once per change of the drawn frame.
|
||||||
|
let wanted = null;
|
||||||
|
let drawnTime = null;
|
||||||
|
const show = (ms) => {
|
||||||
|
wanted = shownTime(ms);
|
||||||
|
requestRender('imagery');
|
||||||
|
};
|
||||||
|
frameFlushes.set(layer, () => {
|
||||||
|
if (wanted === null || wanted === drawnTime) return;
|
||||||
|
drawnTime = wanted;
|
||||||
|
deliver(listeners.frameReady, drawnTime);
|
||||||
|
});
|
||||||
|
layer.setFrames = (next, opts = {}) => {
|
||||||
|
source =
|
||||||
|
next.kind === 'video'
|
||||||
|
? { kind: 'video', segments: next.segments.map((s) => ({ ...s })) }
|
||||||
|
: { kind: 'frames', frames: next.frames.slice() };
|
||||||
|
wanted = null;
|
||||||
|
drawnTime = null;
|
||||||
|
clockKind = opts.clock ?? 'scene';
|
||||||
|
if (clockKind === 'scene') {
|
||||||
|
sceneFrameLayers.set(layer, show);
|
||||||
|
show(sceneTime);
|
||||||
|
} else sceneFrameLayers.delete(layer);
|
||||||
|
requestRender('imagery');
|
||||||
};
|
};
|
||||||
layer.setFrameTime = (ms) => {
|
layer.setFrameTime = (ms) => {
|
||||||
const nearest = frames.reduce(
|
// On the scene clock the frame port's time decides.
|
||||||
(best, f) =>
|
if (clockKind !== 'own') return;
|
||||||
!best || Math.abs(f.time - ms) < Math.abs(best.time - ms)
|
show(ms);
|
||||||
? f
|
requestRender('imagery');
|
||||||
: best,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
if (nearest) deliver(listeners.frameReady, nearest.time);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (index === undefined) imageryLayers.push(layer);
|
if (index === undefined) imageryLayers.push(layer);
|
||||||
@@ -996,7 +1054,7 @@ export function createStubRenderer(options = {}) {
|
|||||||
const globe = {
|
const globe = {
|
||||||
async setBase(base) {
|
async setBase(base) {
|
||||||
log('globe', 'setBase', [base]);
|
log('globe', 'setBase', [base]);
|
||||||
for (const layer of imageryLayers.splice(0)) void layer;
|
for (const layer of imageryLayers.slice()) layer.remove();
|
||||||
if (base.photoreal !== undefined)
|
if (base.photoreal !== undefined)
|
||||||
await globe.setPhotoreal(base.photoreal);
|
await globe.setPhotoreal(base.photoreal);
|
||||||
const layers = (base.imagery ?? []).map((spec) => addImagery(spec));
|
const layers = (base.imagery ?? []).map((spec) => addImagery(spec));
|
||||||
@@ -1008,11 +1066,6 @@ export function createStubRenderer(options = {}) {
|
|||||||
spec && capabilities.has('photoreal')
|
spec && capabilities.has('photoreal')
|
||||||
? { route: 'google' }
|
? { route: 'google' }
|
||||||
: { route: 'none' };
|
: { route: 'none' };
|
||||||
const next = photorealStatus.route === 'none' ? 'globe' : 'tiles';
|
|
||||||
if (next !== imageryHost) {
|
|
||||||
imageryHost = next;
|
|
||||||
deliver(hostListeners, next);
|
|
||||||
}
|
|
||||||
requestRender('base');
|
requestRender('base');
|
||||||
return photorealStatus;
|
return photorealStatus;
|
||||||
},
|
},
|
||||||
@@ -1027,14 +1080,6 @@ export function createStubRenderer(options = {}) {
|
|||||||
imagery: {
|
imagery: {
|
||||||
add: addImagery,
|
add: addImagery,
|
||||||
list: () => imageryLayers.slice(),
|
list: () => imageryLayers.slice(),
|
||||||
host: () => imageryHost,
|
|
||||||
onHostChange(cb) {
|
|
||||||
hostListeners.push(cb);
|
|
||||||
return () => {
|
|
||||||
const i = hostListeners.indexOf(cb);
|
|
||||||
if (i >= 0) hostListeners.splice(i, 1);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
sky: {
|
sky: {
|
||||||
get: () => ({ ...sky }),
|
get: () => ({ ...sky }),
|
||||||
@@ -1058,6 +1103,16 @@ export function createStubRenderer(options = {}) {
|
|||||||
log('globe', 'setSurfaceShading', [shading]);
|
log('globe', 'setSurfaceShading', [shading]);
|
||||||
requestRender('base');
|
requestRender('base');
|
||||||
};
|
};
|
||||||
|
if (capabilities.has('clouds'))
|
||||||
|
globe.setClouds = (clouds) => {
|
||||||
|
log('globe', 'setClouds', [clouds]);
|
||||||
|
requestRender('base');
|
||||||
|
};
|
||||||
|
if (capabilities.has('water'))
|
||||||
|
globe.setWater = (water) => {
|
||||||
|
log('globe', 'setWater', [water]);
|
||||||
|
requestRender('base');
|
||||||
|
};
|
||||||
if (capabilities.has('splitSwipe')) {
|
if (capabilities.has('splitSwipe')) {
|
||||||
globe.imagery.getSplit = () => split;
|
globe.imagery.getSplit = () => split;
|
||||||
globe.imagery.setSplit = (x01) => {
|
globe.imagery.setSplit = (x01) => {
|
||||||
@@ -1328,6 +1383,8 @@ export function createStubRenderer(options = {}) {
|
|||||||
onceTimers.clear();
|
onceTimers.clear();
|
||||||
sets.clear();
|
sets.clear();
|
||||||
satrecs.clear();
|
satrecs.clear();
|
||||||
|
sceneFrameLayers.clear();
|
||||||
|
frameFlushes.clear();
|
||||||
},
|
},
|
||||||
testing: {
|
testing: {
|
||||||
/** Run one frame of the loop; true when it drew. */
|
/** Run one frame of the loop; true when it drew. */
|
||||||
|
|||||||
Reference in new issue
Block a user