mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 11:54:02 +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
|
||||
an icon mid-turn. Height queries that count a set
|
||||
(`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';
|
||||
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. */
|
||||
const ISS_TLE = Object.freeze([
|
||||
'1 25544U 98067A 26004.50000000 .00016717 00000-0 10270-3 0 9992',
|
||||
@@ -1674,11 +1682,161 @@ export function portChecks() {
|
||||
deepEqual(ours(), ['a', 'b'], 'lowered');
|
||||
a.alpha = 0.5;
|
||||
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();
|
||||
deepEqual(ours(), ['b'], 'the removed layer is the one gone');
|
||||
b.remove();
|
||||
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(
|
||||
|
||||
Vendored
+6
@@ -160,6 +160,12 @@ export type Capability =
|
||||
| 'imageryFrames'
|
||||
/** Wind-relief and similar shading of the globe surface. */
|
||||
| '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). */
|
||||
| 'screenModulation'
|
||||
/** GPU frame timing through timer queries. */
|
||||
|
||||
Vendored
+95
-18
@@ -10,13 +10,16 @@ export interface PhotorealSpec {
|
||||
key?: string;
|
||||
/** Renews a session token; called again when the token expires. */
|
||||
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 {
|
||||
/** 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 };
|
||||
}
|
||||
|
||||
@@ -29,7 +32,11 @@ export interface PhotorealStatus {
|
||||
export type TerrainSpec = (
|
||||
| { kind: 'ellipsoid' }
|
||||
| { 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. */
|
||||
| {
|
||||
kind: 'heightmap';
|
||||
@@ -53,11 +60,27 @@ export interface TileRequest {
|
||||
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 {
|
||||
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?: string;
|
||||
/** `'canvas'`: draws one tile. */
|
||||
/** `'canvas'` (`imageHooks`): draws one tile. */
|
||||
drawTile?: (tile: TileRequest, canvas: HTMLCanvasElement) => void;
|
||||
/** WMTS: layer, style, tile matrix set and format. */
|
||||
wmts?: {
|
||||
@@ -77,14 +100,26 @@ export interface ImagerySpec {
|
||||
maxConcurrentRequests?: number;
|
||||
/** HTTP statuses retried rather than treated as missing. */
|
||||
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?: (
|
||||
tile: TileRequest,
|
||||
image: ImageBitmap,
|
||||
) => ImageBitmap | Promise<ImageBitmap>;
|
||||
/**
|
||||
* Decides each tile request: call `issue()` to fetch it, or return
|
||||
* `undefined` to defer it and be asked again on a later frame.
|
||||
* `imageHooks`: decides each tile request on the main thread: call
|
||||
* `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?: (
|
||||
tile: TileRequest,
|
||||
@@ -100,20 +135,56 @@ export interface TimedUrl {
|
||||
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 {
|
||||
alpha: number;
|
||||
show: boolean;
|
||||
/** Which side of the split this layer shows on. */
|
||||
split: 'none' | 'left' | 'right';
|
||||
/** What it drapes onto. `'active'` follows whichever base is showing. */
|
||||
drapeOn: 'globe' | 'tiles' | 'both' | 'active';
|
||||
/** `imageryFrames`: a timed sequence, prefetched hidden. */
|
||||
/**
|
||||
* What it drapes onto: the globe's surface (terrain or the ellipsoid),
|
||||
* 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?(
|
||||
frames: TimedUrl[],
|
||||
options?: { mode?: 'nearest' | 'blend'; prefetch?: number },
|
||||
source: TimedSource,
|
||||
options?: {
|
||||
mode?: 'nearest' | 'blend';
|
||||
prefetch?: number;
|
||||
clock?: 'scene' | 'own';
|
||||
},
|
||||
): void;
|
||||
/** `imageryFrames`: which time to show. */
|
||||
/** `imageryFrames`: which time to show, on the `own` clock. */
|
||||
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: 'tileError',
|
||||
@@ -189,12 +260,18 @@ export interface GlobePort {
|
||||
setGlobeVisible(visible: boolean): void;
|
||||
/** `surfaceShading`. */
|
||||
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: {
|
||||
/**
|
||||
* 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;
|
||||
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. */
|
||||
getSplit?(): number;
|
||||
/** `splitSwipe`: move the split to `x01` of the canvas's CSS width. */
|
||||
|
||||
@@ -23,6 +23,9 @@ export const CAPABILITIES = Object.freeze([
|
||||
'splitSwipe',
|
||||
'imageryFrames',
|
||||
'surfaceShading',
|
||||
'clouds',
|
||||
'water',
|
||||
'imageHooks',
|
||||
'screenModulation',
|
||||
'gpuTiming',
|
||||
'pickPosition',
|
||||
@@ -138,8 +141,6 @@ export const PORTS = deepFreeze({
|
||||
'imagery',
|
||||
'imagery.add',
|
||||
'imagery.list',
|
||||
'imagery.host',
|
||||
'imagery.onHostChange',
|
||||
'sky',
|
||||
'sky.get',
|
||||
'sky.set',
|
||||
@@ -147,6 +148,8 @@ export const PORTS = deepFreeze({
|
||||
'on',
|
||||
),
|
||||
['setSurfaceShading', 'surfaceShading'],
|
||||
['setClouds', 'clouds'],
|
||||
['setWater', 'water'],
|
||||
['imagery.getSplit', 'splitSwipe'],
|
||||
['imagery.setSplit', 'splitSwipe'],
|
||||
]),
|
||||
@@ -293,6 +296,7 @@ export const PORTS = deepFreeze({
|
||||
'show',
|
||||
'split',
|
||||
'drapeOn',
|
||||
'lit',
|
||||
'on',
|
||||
'raise',
|
||||
'lower',
|
||||
|
||||
Vendored
+6
-2
@@ -34,8 +34,12 @@ export interface CreateOptions {
|
||||
googleApiKey?: string;
|
||||
/** Short-lived Google session tokens, when the key is held server-side. */
|
||||
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. */
|
||||
msaa?: 0 | 2 | 4 | 8;
|
||||
/** Keep the drawing buffer after presenting, for capture. */
|
||||
|
||||
+85
-28
@@ -201,6 +201,9 @@ export function createStubRenderer(options = {}) {
|
||||
runPhase('preRender', t);
|
||||
step('render');
|
||||
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');
|
||||
runPhase('postRender', t);
|
||||
for (const waiter of onceWaiters.splice(0)) waiter();
|
||||
@@ -277,6 +280,7 @@ export function createStubRenderer(options = {}) {
|
||||
},
|
||||
setTime(ms) {
|
||||
sceneTime = ms;
|
||||
for (const show of [...sceneFrameLayers.values()]) show(ms);
|
||||
requestRender('time');
|
||||
},
|
||||
time: () => sceneTime,
|
||||
@@ -916,8 +920,10 @@ export function createStubRenderer(options = {}) {
|
||||
|
||||
const imageryLayers = [];
|
||||
let split = 0.5;
|
||||
let imageryHost = 'globe';
|
||||
const hostListeners = [];
|
||||
/** Layers whose frames follow scene time, each with its show-a-time function. */
|
||||
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 settled = { base: true, tiles: true, terrain: true, imagery: true };
|
||||
let sky = {
|
||||
@@ -933,13 +939,21 @@ export function createStubRenderer(options = {}) {
|
||||
let photorealStatus = { route: 'none' };
|
||||
|
||||
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 layer = {
|
||||
spec: { ...spec },
|
||||
alpha: 1,
|
||||
show: true,
|
||||
split: 'none',
|
||||
drapeOn: 'active',
|
||||
drapeOn: 'all',
|
||||
lit: true,
|
||||
on(event, cb) {
|
||||
listeners[event]?.push(cb);
|
||||
return () => {
|
||||
@@ -967,23 +981,67 @@ export function createStubRenderer(options = {}) {
|
||||
remove() {
|
||||
const i = imageryLayers.indexOf(layer);
|
||||
if (i >= 0) imageryLayers.splice(i, 1);
|
||||
sceneFrameLayers.delete(layer);
|
||||
frameFlushes.delete(layer);
|
||||
requestRender('imagery');
|
||||
},
|
||||
};
|
||||
if (capabilities.has('imageryFrames')) {
|
||||
let frames = [];
|
||||
layer.setFrames = (list) => {
|
||||
frames = list.slice();
|
||||
let source = null;
|
||||
let clockKind = 'scene';
|
||||
/** 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) => {
|
||||
const nearest = frames.reduce(
|
||||
(best, f) =>
|
||||
!best || Math.abs(f.time - ms) < Math.abs(best.time - ms)
|
||||
? f
|
||||
: best,
|
||||
null,
|
||||
);
|
||||
if (nearest) deliver(listeners.frameReady, nearest.time);
|
||||
// On the scene clock the frame port's time decides.
|
||||
if (clockKind !== 'own') return;
|
||||
show(ms);
|
||||
requestRender('imagery');
|
||||
};
|
||||
}
|
||||
if (index === undefined) imageryLayers.push(layer);
|
||||
@@ -996,7 +1054,7 @@ export function createStubRenderer(options = {}) {
|
||||
const globe = {
|
||||
async 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)
|
||||
await globe.setPhotoreal(base.photoreal);
|
||||
const layers = (base.imagery ?? []).map((spec) => addImagery(spec));
|
||||
@@ -1008,11 +1066,6 @@ export function createStubRenderer(options = {}) {
|
||||
spec && capabilities.has('photoreal')
|
||||
? { route: 'google' }
|
||||
: { route: 'none' };
|
||||
const next = photorealStatus.route === 'none' ? 'globe' : 'tiles';
|
||||
if (next !== imageryHost) {
|
||||
imageryHost = next;
|
||||
deliver(hostListeners, next);
|
||||
}
|
||||
requestRender('base');
|
||||
return photorealStatus;
|
||||
},
|
||||
@@ -1027,14 +1080,6 @@ export function createStubRenderer(options = {}) {
|
||||
imagery: {
|
||||
add: addImagery,
|
||||
list: () => imageryLayers.slice(),
|
||||
host: () => imageryHost,
|
||||
onHostChange(cb) {
|
||||
hostListeners.push(cb);
|
||||
return () => {
|
||||
const i = hostListeners.indexOf(cb);
|
||||
if (i >= 0) hostListeners.splice(i, 1);
|
||||
};
|
||||
},
|
||||
},
|
||||
sky: {
|
||||
get: () => ({ ...sky }),
|
||||
@@ -1058,6 +1103,16 @@ export function createStubRenderer(options = {}) {
|
||||
log('globe', 'setSurfaceShading', [shading]);
|
||||
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')) {
|
||||
globe.imagery.getSplit = () => split;
|
||||
globe.imagery.setSplit = (x01) => {
|
||||
@@ -1328,6 +1383,8 @@ export function createStubRenderer(options = {}) {
|
||||
onceTimers.clear();
|
||||
sets.clear();
|
||||
satrecs.clear();
|
||||
sceneFrameLayers.clear();
|
||||
frameFlushes.clear();
|
||||
},
|
||||
testing: {
|
||||
/** Run one frame of the loop; true when it drew. */
|
||||
|
||||
Reference in new issue
Block a user