Replace Cesium monkey-patches with hooks and keep internals in one module

The arrival ground guard replaced camera.flyTo, flyToBoundingSphere,
setView, lookAt and cancelFlight with wrappers so a direct caller would stop
it. It now remembers the pose it started on and, before each retry, yields
if a flight is under way or the camera has moved across the ground, down or
around. A straight rise is not a move: Cesium's terrain collision lifts a
buried eye that way as tiles load, which is the case the guard exists for.
This also catches owners the wrappers missed (a pose written directly,
lookAtTransform), and no camera method is replaced.

Recent imagery and weather overwrote provider.requestImage (weather also
provider.credit) to ration tiles. They now state that policy as a hook with
withTileRequestHook(provider, { requestImage, credit }) from
src/render/cesium/tileRequests.js, which hands Cesium a provider delegating
to the original and leaves the original untouched.

Every private Cesium field Core reaches goes through a named helper in
src/render/cesium/internals.js: the main and offscreen pick views and their
command extents, a billboard's collection, a cluster's point collection,
the widget's render-loop latch, the WebGL context and the current camera
flight. internals.test.mjs fails when the installed Cesium stops declaring
one of them, and a source scan fails on private access anywhere else. The
cyber sonar's primitives.update wrap stays until the effects work.

Live (Austin, keyless globe): a buried eye is lifted to 253 m over a 133 m
surface; a setView or flyTo by another caller makes the guard yield; radar
loads its tiles through the hook. Documented in docs/APPLICATION.md and
docs/CURRENT-STATE.md.
This commit is contained in:
Sameh Khamis committed 2026-11-04 11:15:35 -08:00
1 parent d51ea4007b
commit ad8adc85f4
18 files changed
+597 -125

No files matched your search

+12
View File
@@ -163,6 +163,18 @@ a Cesium throw for missing depth-texture support, and every caller treats it
as one. Cesium answers `now()` synchronously from the scene, so it costs what
the direct call did.
## Cesium internals
Core reaches private Cesium fields only through
`src/render/cesium/internals.js`, one named helper per field (the main and
offscreen pick views, a billboard's collection, a cluster's point collection,
the widget's render-loop latch, the WebGL context and whether a camera flight
is under way). `internals.test.mjs` fails when the installed Cesium stops
declaring one of them, and when a private field is reached anywhere else.
Imagery that rations its tile requests states the policy as a hook with
`withTileRequestHook(provider, { requestImage, credit })` from
`src/render/cesium/tileRequests.js`, which leaves the provider untouched.
## Standalone wiring
`src/main.js` reads the existing browser configuration and starts
+4 -4
View File
@@ -1414,7 +1414,7 @@ and a Denver coordinate underground. Search now resolves the application's
ground-floor elevation (bounded wait, same ellipsoidal datum) before framing and
warms the cell; after landing, `src/cameraGroundGuard.js` samples the rendered
surface under the target and the camera once tiles stream in and lifts the eye
to 120 m clearance, holding heading and pitch. Navigation ownership changes (Director, tracking, keyboard/UI controls and direct camera flights), layer/app teardown, and pointer/wheel gestures cancel the check and remove its listeners. `scripts/qa-context-arrivals.mjs` types Camp Mabry
to 120 m clearance, holding heading and pitch. Navigation ownership changes (Director, tracking, keyboard/UI controls), any other caller moving the camera or starting a flight before a retry, layer/app teardown, and pointer/wheel gestures cancel the check and remove its listeners; the guard compares the pose it started on rather than wrapping camera methods. `scripts/qa-context-arrivals.mjs` types Camp Mabry
and a Denver coordinate and requires at least 60 m clearance.
## Layer panel ownership
@@ -2396,9 +2396,9 @@ which embeds the MAP_KEY.
> so a turned-off layer's entities stayed reachable (Data Centers kept 116
> MiB). `src/sceneCommandRelease.js`, installed in `src/app/tools.js`,
> requests one frame through `governorRequestRender` after every layer
> disable and then trims those arrays. It reads private Cesium fields;
> `src/sceneCommandRelease.test.mjs` pins them and fails if Cesium renames
> them or changes which passes it truncates.
> disable and then trims those arrays. It reads private Cesium fields through
> `src/render/cesium/internals.js`; `src/sceneCommandRelease.test.mjs` pins
> them and fails if Cesium renames them or changes which passes it truncates.
> **2026-08-08 — performance waves 1+2:** the app idles via an explicit render
> governor (`src/renderGovernor.js` — hold/release from every per-frame
+4
View File
@@ -48,6 +48,10 @@
"src/input/sensorPick.js",
"src/input/dispatcher.js",
"src/queries/heights.js",
"src/render/cesium/internals.js",
"src/render/cesium/internals.test.mjs",
"src/render/cesium/tileRequests.js",
"src/render/cesium/tileRequests.test.mjs",
"src/queries/heights.test.mjs",
"docs/CODE-BOUNDARIES.md",
"src/main.js",
+16 -7
View File
@@ -1370,7 +1370,8 @@
"src/input/sensorPick.js",
"src/data/scenePick.js",
"src/data/pickRegistry.js",
"src/input/dispatcher.js"
"src/input/dispatcher.js",
"src/render/cesium/internals.js"
],
"external": ["cesium"]
},
@@ -2146,7 +2147,8 @@
"src/data/scenePick.js",
"src/input/sensorPick.js",
"src/data/pickRegistry.js",
"src/input/dispatcher.js"
"src/input/dispatcher.js",
"src/render/cesium/internals.js"
],
"external": ["cesium"]
},
@@ -3333,7 +3335,9 @@
"src/data/entityRecords.js",
"src/input/sensorPick.js",
"src/input/dispatcher.js",
"src/queries/heights.js"
"src/queries/heights.js",
"src/render/cesium/internals.js",
"src/render/cesium/tileRequests.js"
],
"external": [
"hls.js",
@@ -3959,7 +3963,9 @@
"src/data/entityRecords.js",
"src/input/sensorPick.js",
"src/input/dispatcher.js",
"src/queries/heights.js"
"src/queries/heights.js",
"src/render/cesium/tileRequests.js",
"src/render/cesium/internals.js"
],
"external": [
"hls.js",
@@ -4047,7 +4053,8 @@
"src/data/placeNames.js",
"src/data/local_data/us_census_places/files.js",
"src/data/local_data/wof_neighborhoods/files.js",
"src/queries/heights.js"
"src/queries/heights.js",
"src/render/cesium/internals.js"
],
"external": [
"cesium",
@@ -4656,7 +4663,8 @@
"src/i18n/locales/zh-Hant.json",
"src/input/sensorPick.js",
"src/input/dispatcher.js",
"src/queries/heights.js"
"src/queries/heights.js",
"src/render/cesium/internals.js"
],
"external": ["cesium", "egm96-universal", "pbf"]
},
@@ -4713,7 +4721,8 @@
"src/data/scenePick.js",
"src/data/pickRegistry.js",
"src/input/dispatcher.js",
"src/queries/heights.js"
"src/queries/heights.js",
"src/render/cesium/internals.js"
],
"external": ["cesium", "pbf"]
},
+57 -27
View File
@@ -17,6 +17,7 @@
import * as Cesium from 'cesium';
import { ownCameraArrival } from './data/cameraArrival.js';
import { heightsFor } from './queries/heights.js';
import { cameraFlightActive } from './render/cesium/internals.js';
/** Minimum eye height above the rendered surface for a usable view. */
export const MIN_EYE_CLEARANCE_M = 120;
@@ -25,6 +26,11 @@ const GUARD_ATTEMPTS = 8;
const GUARD_INTERVAL_MS = 600;
/** Ignore sub-metre noise rather than nudging the camera forever. */
const GUARD_EPSILON_M = 2;
/** Eye travel (m) and turn (cosine) that mean another owner moved the camera. */
const MOVED_M = 0.01;
const TURNED_COS = 1 - 1e-9;
const groundScratch = new Cesium.Cartesian3();
const probeScratch = new Cesium.Cartographic();
@@ -52,6 +58,46 @@ function sampleSurfaceM(scene, cells) {
return { heightM, sampled };
}
/** The eye's ground point (height 0), its height, and which way it looks. */
function poseOf(camera) {
const carto =
camera?.positionWC && Cesium.Cartographic.fromCartesian(camera.positionWC);
if (!carto || !camera.directionWC || !camera.upWC) return null;
return {
ground: Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude),
height: carto.height,
direction: Cesium.Cartesian3.clone(camera.directionWC),
up: Cesium.Cartesian3.clone(camera.upWC),
};
}
/**
* Whether another owner has moved the camera off `pose`: across the ground,
* down, or around. A pure rise is not one: Cesium's terrain collision lifts a
* buried eye straight up as tiles load, which is the case the guard exists
* for, so `pose` follows the highest eye seen and the guard keeps measuring.
*/
function moved(camera, pose) {
const carto = Cesium.Cartographic.fromCartesian(camera.positionWC);
if (!carto) return true;
const ground = Cesium.Cartesian3.fromRadians(
carto.longitude,
carto.latitude,
0,
undefined,
groundScratch,
);
if (
Cesium.Cartesian3.distance(ground, pose.ground) > MOVED_M ||
carto.height < pose.height - MOVED_M ||
Cesium.Cartesian3.dot(camera.directionWC, pose.direction) < TURNED_COS ||
Cesium.Cartesian3.dot(camera.upWC, pose.up) < TURNED_COS
)
return true;
pose.height = Math.max(pose.height, carto.height);
return false;
}
/**
* How far the camera must rise to clear the ground, given a measured surface.
* Pure so the policy is testable without a scene.
@@ -75,8 +121,10 @@ export function groundClearanceDeficitM(
* Watch an arrival and lift the camera once the real surface can be measured.
*
* Stops quietly on a clear view, exhausted attempts, the user taking the
* controls (pointer or wheel on the canvas), or `isStale()` reporting that a
* newer arrival owns the camera.
* controls (pointer or wheel on the canvas), `isStale()` reporting that a
* newer arrival owns the camera, or any other caller moving the camera or
* starting a flight in the meantime: the guard only ever corrects the pose it
* was started on.
* @param {Cesium.Viewer} viewer Live viewer.
* @param {{lat: number, lon: number}} at Destination being framed.
* @param {{clearanceM?: number, attempts?: number, intervalMs?: number,
@@ -94,6 +142,10 @@ export function guardCameraAboveGround(viewer, at, options = {}) {
if (!viewer?.scene || !Number.isFinite(at?.lat) || !Number.isFinite(at?.lon))
return () => {};
// Without a readable pose the guard could not tell a later owner's move.
const arrival = poseOf(viewer.camera);
if (!arrival) return () => {};
let timer = null;
let done = false;
const canvas = viewer.scene.canvas;
@@ -111,30 +163,6 @@ export function guardCameraAboveGround(viewer, at, options = {}) {
release();
};
release = ownCameraArrival(viewer, stop);
// Direct camera API callers (including duration-zero flights) must yield
// before moving the eye. Restore the exact method ownership on completion.
for (const name of [
'flyTo',
'flyToBoundingSphere',
'setView',
'lookAt',
'cancelFlight',
]) {
const camera = viewer.camera;
const original = camera[name];
if (typeof original !== 'function') continue;
const descriptor = Object.getOwnPropertyDescriptor(camera, name);
const wrapped = function (...args) {
stop();
return original.apply(this, args);
};
camera[name] = wrapped;
removers.push(() => {
if (camera[name] !== wrapped) return;
if (descriptor) Object.defineProperty(camera, name, descriptor);
else delete camera[name];
});
}
const removeTracking = viewer.trackedEntityChanged?.addEventListener(stop);
if (removeTracking) removers.push(removeTracking);
documentRef?.addEventListener?.('keydown', stop, true);
@@ -146,8 +174,10 @@ export function guardCameraAboveGround(viewer, at, options = {}) {
const attempt = () => {
if (done) return;
if (isStale() || viewer.isDestroyed?.()) return stop();
remaining -= 1;
const camera = viewer.camera;
// Another caller owns the camera now; lifting would undo its move.
if (cameraFlightActive(camera) || moved(camera, arrival)) return stop();
remaining -= 1;
const carto = camera.positionCartographic;
// The eye must clear both the framed target and the ground beneath it.
const below = {
+60 -4
View File
@@ -59,10 +59,17 @@ function fakeViewer({ cameraHeightM, surfaceM }) {
},
camera: {
positionCartographic: position,
positionWC: Cesium.Cartographic.toCartesian(position),
directionWC: new Cesium.Cartesian3(0, 0, -1),
upWC: new Cesium.Cartesian3(0, 1, 0),
heading: 0.5,
pitch: -0.55,
roll: 0,
flyTo: (options) => flights.push(options),
// As Cesium does: a flight is under way until it completes.
flyTo(options) {
flights.push(options);
this._currentFlight = {};
},
},
};
}
@@ -122,8 +129,16 @@ function handoffFixture(t) {
viewer.trackedEntityChanged = new Cesium.Event();
viewer.camera.cancelFlight = () => {};
viewer.camera.lookAtTransform = () => {};
viewer.camera.flyToBoundingSphere = () => {};
viewer.camera.setView = () => {};
viewer.camera.flyToBoundingSphere = function () {
this._currentFlight = {};
};
viewer.camera.setView = function () {
this.positionWC = Cesium.Cartesian3.add(
this.positionWC,
new Cesium.Cartesian3(0, 0, 50),
new Cesium.Cartesian3(),
);
};
const originalFly = viewer.camera.flyTo;
const navigation = new NavigationController({
viewer,
@@ -142,6 +157,8 @@ for (const handoff of ['Director', 'tracking', 'follow', 'keyboard', 'UI',
'flyTo', 'flyToBoundingSphere', 'setView', 'layer-disable', 'layer-destroy', 'app-stop']) {
test(`arrival guard relinquishes and removes hooks on ${handoff}`, async (t) => {
const { viewer, navigation, originalFly } = handoffFixture(t);
const methods = ['flyTo', 'flyToBoundingSphere', 'setView', 'lookAt', 'cancelFlight']
.map((name) => viewer.camera[name]);
if (handoff === 'Director') {
SceneDirector.prototype._claimCameraOwnership.call({
viewer, styleManager: { runImmediateNavigation: (noun, fn) =>
@@ -161,11 +178,50 @@ for (const handoff of ['Director', 'tracking', 'follow', 'keyboard', 'UI',
if (handoff === 'layer-disable') await lifecycle.setEnabled('test', false);
else await lifecycle.destroyLayer('test');
} else viewer.camera[handoff]({});
// A direct flyTo is itself one recorded flight; the guard adds none.
const priorFlights = viewer.flights.length;
t.mock.timers.tick(10000);
assert.equal(viewer.flights.length, priorFlights, 'the old probe cannot fly');
assert.equal(viewer.camera.flyTo, originalFly, 'direct-call hook is removed');
assert.equal(viewer.camera.flyTo, originalFly);
assert.deepEqual(
['flyTo', 'flyToBoundingSphere', 'setView', 'lookAt', 'cancelFlight']
.map((name) => viewer.camera[name]),
methods,
'the guard never replaces camera methods',
);
assert.equal(viewer.trackedEntityChanged.numberOfListeners, 0);
navigation.destroy();
});
}
test('the guard yields to a camera moved without any camera method', async () => {
const viewer = fakeViewer({ cameraHeightM: 172, surfaceM: 171 });
guardCameraAboveGround(viewer, { lat: 30.3125, lon: -97.765 }, { intervalMs: 5 });
// A cockpit or follow frame writes the pose directly.
viewer.camera.directionWC = new Cesium.Cartesian3(1, 0, 0);
await new Promise((resolve) => setTimeout(resolve, 20));
assert.equal(viewer.flights.length, 0, 'the moved view is not corrected');
});
test('Cesium lifting a buried eye straight up is not another owner', async () => {
const viewer = fakeViewer({ cameraHeightM: 104, surfaceM: 171 });
guardCameraAboveGround(viewer, { lat: 30.3125, lon: -97.765 }, { intervalMs: 5 });
// Terrain collision raises the eye to just above the loaded ground.
const raised = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, 172);
viewer.camera.positionCartographic = raised;
viewer.camera.positionWC = Cesium.Cartographic.toCartesian(raised);
await new Promise((resolve) => setTimeout(resolve, 30));
assert.equal(viewer.flights.length, 1, 'the guard still lifts the eye');
const lifted = Cesium.Cartographic.fromCartesian(viewer.flights[0].destination);
assert.ok(Math.abs(lifted.height - (171 + MIN_EYE_CLEARANCE_M)) < 0.5);
});
test('another owner lowering the eye is not corrected', async () => {
const viewer = fakeViewer({ cameraHeightM: 172, surfaceM: 171 });
guardCameraAboveGround(viewer, { lat: 30.3125, lon: -97.765 }, { intervalMs: 5 });
const lowered = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, 150);
viewer.camera.positionCartographic = lowered;
viewer.camera.positionWC = Cesium.Cartographic.toCartesian(lowered);
await new Promise((resolve) => setTimeout(resolve, 30));
assert.equal(viewer.flights.length, 0);
});
+2 -2
View File
@@ -1,6 +1,7 @@
import { approxDistanceKm as _approxDistanceKm } from './recordPolicy.js';
import { displayedKinematics } from '../../data/motionModel.js';
import * as Cesium from 'cesium';
import { closeRenderLoopLatch } from '../../render/cesium/internals.js';
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
import { modelVisualAnchor } from '../../data/modelVisualAnchor.js';
import {
@@ -922,8 +923,7 @@ export function createQueries({
// while manual evidence renders can begin without waiting on VSYNC.
return new Promise((resolve) =>
setTimeout(() => {
if (flightState._viewer?._cesiumWidget)
flightState._viewer._cesiumWidget._renderLoopRunning = false;
closeRenderLoopLatch(flightState._viewer);
resolve({ ok: true, nowMs: startMs });
}, 0),
);
+2 -1
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import { clusterPointCollection as clusterPointsOf } from '../../render/cesium/internals.js';
import { horizonOccluder } from '../../data/iconOrientation.js';
import {
MARKER_LIFT_M,
@@ -81,7 +82,7 @@ export function createRendering({
}
function clusterPointCollection() {
return layerState._dataSource?.clustering?._clusterPointCollection || null;
return clusterPointsOf(layerState._dataSource?.clustering);
}
function publishRadioOverlayEntries() {
+27 -26
View File
@@ -7,6 +7,7 @@
* small semaphore of our own; Cesium's global request limits stay untouched.
*/
import * as Cesium from 'cesium';
import { withTileRequestHook } from '../../render/cesium/tileRequests.js';
import { PRODUCTS, gibsTemplate } from './model.js';
const SLOT_IDS = ['a', 'b'];
@@ -27,7 +28,7 @@ const boxKey = (box) =>
[box.west, box.south, box.east, box.north].map(Number).join(',');
/**
* Gate a provider's `requestImage` through the semaphore. Returning
* Gate a provider's tile requests through the semaphore. Returning
* `undefined` tells Cesium to retry on a later frame, but under
* `requestRenderMode` there is no later frame unless something asks — and a
* deferral must never ask for one itself: the retried tile would defer again
@@ -37,31 +38,31 @@ const boxKey = (box) =>
* deferred goes through the one coalesced delayed retry.
*/
function admitTileRequests(provider, admission, requestRender, retry) {
const original = provider.requestImage.bind(provider);
provider.requestImage = (x, y, level, request) => {
if (admission.inFlight >= admission.limit) return undefined;
admission.inFlight += 1;
const release = () => {
admission.inFlight -= 1;
};
let promise;
try {
promise = original(x, y, level, request);
} catch (error) {
release();
throw error;
}
if (!promise) {
release();
retry.schedule();
return undefined;
}
return Promise.resolve(promise).finally(() => {
release();
requestRender(TILE_RENDER_REASON);
});
};
return provider;
return withTileRequestHook(provider, {
requestImage: (_tile, issue) => {
if (admission.inFlight >= admission.limit) return undefined;
admission.inFlight += 1;
const release = () => {
admission.inFlight -= 1;
};
let promise;
try {
promise = issue();
} catch (error) {
release();
throw error;
}
if (!promise) {
release();
retry.schedule();
return undefined;
}
return Promise.resolve(promise).finally(() => {
release();
requestRender(TILE_RENDER_REASON);
});
},
});
}
/**
+3 -2
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import { billboardCollectionOf } from '../../render/cesium/internals.js';
import {
SELECTED_CARD_REFRESH_MS,
TRANSIT_SELECTED_OVERLAY_SOURCE_ID,
@@ -130,9 +131,9 @@ export function createSelection({ state, services, parts }) {
x: position.x,
y: position.y,
collection:
picked?.primitive?._billboardCollection === state._animatedMarkers
billboardCollectionOf(picked?.primitive) === state._animatedMarkers
? 'animated'
: picked?.primitive?._billboardCollection === state._markers
: billboardCollectionOf(picked?.primitive) === state._markers
? 'stationary'
: null,
};
+2 -1
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import { webglContext } from '../../render/cesium/internals.js';
import { recordFix, updatePlayback, attachFloorToPlace } from './movement.js';
import { seek, setRate, mergeHistory } from '../../data/contactPlayback.js';
import { getRegisteredTransitFeed } from '../../data/transitFeeds.js';
@@ -166,7 +167,7 @@ export function createTesting({ state, parts }) {
stride = 12,
data = new Float64Array(capacity * stride);
const previousPositions = new Float64Array(state._vehicles.size * 3);
const gl = scene.context._gl;
const gl = webglContext(scene);
let active = false,
upload = 0,
cpu = 0,
+41 -43
View File
@@ -2,6 +2,7 @@ import { weatherTileUrl } from './source.js';
import { orderWeatherImagery } from './imageryOrder.js';
import { imageryHostStatus } from './imageryHost.js';
import { createRasterTileProvider } from './rasterTiles.js';
import { withTileRequestHook } from '../../render/cesium/tileRequests.js';
import { createWeatherShell } from './shellRendering.js';
import { weatherDisplay, escapeCreditText } from './display.js';
import { msg } from '../../i18n/index.js';
@@ -385,7 +386,7 @@ function createGlobeRendering({
);
setCredit(frame);
const credit = frame.credit;
const provider = global
const source = global
? createRasterTileProvider({
cesium,
texture,
@@ -408,49 +409,46 @@ function createGlobeRendering({
enablePickFeatures: false,
credit,
});
// Cesium reads a provider's credit every frame; a refreshed manifest
// can replace it without restaging the imagery.
Object.defineProperty(provider, 'credit', {
configurable: true,
enumerable: true,
get: () => frame.credit,
});
const requestImage = provider.requestImage.bind(provider);
provider.requestImage = (x, y, level, request) => {
if (frame.closed) return undefined;
const result = requestImage(x, y, level, request);
const tileKey = `${level}/${x}/${y}`;
if (!result) {
// Scheduler admission is part of readiness, not a successful tile.
frame.deferred.add(tileKey);
frame.lastActivity = now();
return result;
}
frame.deferred.delete(tileKey);
frame.pending++;
frame.lastActivity = now();
if (request) frame.requests.add(request);
return Promise.resolve(result)
.then((image) => {
// Cesium decodes tiles as ImageBitmaps already flipped, since WebGL
// ignores UNPACK_FLIP_Y for them; a canvas upload flips again.
if (!frame.closed && snapshot.product === 'clouds-regional')
image = processInfraredImage(image, infrared, createCanvas, {
flipY:
typeof ImageBitmap !== 'undefined' &&
image instanceof ImageBitmap,
});
frame.retries.delete(tileKey);
frame.loaded++;
return image;
})
.finally(() => {
frame.pending--;
const provider = withTileRequestHook(source, {
// Cesium reads a provider's credit every frame; a refreshed manifest
// can replace it without restaging the imagery.
credit: () => frame.credit,
requestImage: ({ x, y, level, request }, issue) => {
if (frame.closed) return undefined;
const result = issue();
const tileKey = `${level}/${x}/${y}`;
if (!result) {
// Scheduler admission is part of readiness, not a successful tile.
frame.deferred.add(tileKey);
frame.lastActivity = now();
frame.requests.delete(request);
if (!frame.closed) viewer.scene.requestRender();
});
};
return result;
}
frame.deferred.delete(tileKey);
frame.pending++;
frame.lastActivity = now();
if (request) frame.requests.add(request);
return Promise.resolve(result)
.then((image) => {
// Cesium decodes tiles as ImageBitmaps already flipped, since WebGL
// ignores UNPACK_FLIP_Y for them; a canvas upload flips again.
if (!frame.closed && snapshot.product === 'clouds-regional')
image = processInfraredImage(image, infrared, createCanvas, {
flipY:
typeof ImageBitmap !== 'undefined' &&
image instanceof ImageBitmap,
});
frame.retries.delete(tileKey);
frame.loaded++;
return image;
})
.finally(() => {
frame.pending--;
frame.lastActivity = now();
frame.requests.delete(request);
if (!frame.closed) viewer.scene.requestRender();
});
},
});
frame.offError = provider.errorEvent.addEventListener((error) => {
if (frame.closed) return;
const status = error?.error?.statusCode;
+14 -4
View File
@@ -100,16 +100,26 @@ function renderingHarness(options = {}) {
const providers = [];
const postRender = event();
let renderRequests = 0;
// `providers` lists what Cesium is handed, in order. The layer may hand it
// a hook over this provider, so per-test state lives in one box both see.
class Provider {
constructor(options) {
this.options = options;
this.tilingScheme = new GeographicTilingScheme();
this.errorEvent = event();
this.response = null;
providers.push(this);
this.box = { response: null, requestArgs: undefined };
}
get response() {
return this.box.response;
}
set response(value) {
this.box.response = value;
}
get requestArgs() {
return this.box.requestArgs;
}
requestImage(...args) {
this.requestArgs = args;
this.box.requestArgs = args;
return this.response?.promise;
}
}
@@ -2345,7 +2355,7 @@ for (const product of ['radar', 'clouds-regional', 'lightning']) {
positionCartographic: { height: 60_000 },
};
const pending = h.rendering.setFrame({ ...snapshot, product }, times[0]);
const options = h.providers[0].options;
const options = collection.get(0).imageryProvider.options;
assert.equal(options.tileWidth, 256);
assert.equal(options.tileHeight, 256);
assert.equal(options.maximumLevel, 6);
+86
View File
@@ -0,0 +1,86 @@
/**
* @module render/cesium/internals
* @description Every read or write of a private Cesium field, in one place.
*
* Core otherwise uses Cesium's public API. Each helper here names one private
* field Core depends on and says why; `internals.test.mjs` fails when the
* installed Cesium no longer declares one of them, and a source scan fails
* when a private field is reached anywhere else.
*/
/**
* The scene's main view, whose per-pass command arrays outlive a frame.
* Used by `sceneCommandRelease.js` to drop references to destroyed commands.
* @param {object} scene Cesium Scene.
* @returns {object|undefined} Cesium `View`.
*/
export function defaultView(scene) {
return scene?._defaultView;
}
/**
* The offscreen view `sampleHeight` and `pickFromRay` render into.
* @param {object} scene Cesium Scene.
* @returns {object|undefined} Cesium `View`.
*/
export function pickOffscreenView(scene) {
return scene?._picking?._pickOffscreenView;
}
/**
* A view's command-extent records, each holding the command it last drew.
* @param {object} view Cesium `View`.
* @returns {Array<{command: object}>}
*/
export function commandExtents(view) {
return view?._commandExtents || [];
}
/**
* The collection a picked billboard belongs to; Cesium's pick result names
* the billboard, not its collection.
* @param {object} billboard Cesium Billboard.
* @returns {object|undefined} Cesium BillboardCollection.
*/
export function billboardCollectionOf(billboard) {
return billboard?._billboardCollection;
}
/**
* The point collection an `EntityCluster` draws its cluster markers into.
* @param {object} clustering Cesium EntityCluster.
* @returns {object|null} Cesium PointPrimitiveCollection.
*/
export function clusterPointCollection(clustering) {
return clustering?._clusterPointCollection || null;
}
/**
* Close the widget's pending render-loop latch after `useDefaultRenderLoop`
* is set false, so a callback already queued sees the loop as stopped and
* manual renders start without waiting a vsync.
* @param {object} viewer Cesium Viewer.
*/
export function closeRenderLoopLatch(viewer) {
const widget = viewer?._cesiumWidget;
if (widget) widget._renderLoopRunning = false;
}
/**
* The scene's WebGL context, for diagnostics that read the GPU directly.
* @param {object} scene Cesium Scene.
* @returns {WebGLRenderingContext|WebGL2RenderingContext|undefined}
*/
export function webglContext(scene) {
return scene?.context?._gl;
}
/**
* Whether a camera flight is under way. Cesium clears it before calling a
* flight's `complete`, so it reads false inside that callback.
* @param {object} camera Cesium Camera.
* @returns {boolean}
*/
export function cameraFlightActive(camera) {
return camera?._currentFlight != null;
}
+140
View File
@@ -0,0 +1,140 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import {
billboardCollectionOf,
cameraFlightActive,
closeRenderLoopLatch,
clusterPointCollection,
commandExtents,
defaultView,
pickOffscreenView,
webglContext,
} from './internals.js';
const ROOT = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'..',
);
const require = createRequire(import.meta.url);
const packageSource = (name) =>
path.join(path.dirname(require.resolve(`${name}/package.json`)), 'Source');
const ENGINE = packageSource('@cesium/engine');
const WIDGETS = packageSource('@cesium/widgets');
test('every private field Core reaches is still declared by the installed Cesium', () => {
const fields = [
[ENGINE, 'Scene/Scene.js', '_defaultView'],
[ENGINE, 'Scene/Scene.js', '_picking'],
[ENGINE, 'Scene/Picking.js', '_pickOffscreenView'],
[ENGINE, 'Scene/View.js', '_commandExtents'],
[ENGINE, 'Scene/Billboard.js', '_billboardCollection'],
[ENGINE, 'DataSources/EntityCluster.js', '_clusterPointCollection'],
[ENGINE, 'Widget/CesiumWidget.js', '_renderLoopRunning'],
[WIDGETS, 'Viewer/Viewer.js', '_cesiumWidget'],
[ENGINE, 'Renderer/Context.js', '_gl'],
[ENGINE, 'Scene/Camera.js', '_currentFlight'],
];
const missing = fields.filter(
([root, file, field]) =>
!new RegExp(`this\\.${field}\\s*=`).test(
fs.readFileSync(path.join(root, file), 'utf8'),
),
);
assert.deepEqual(missing, []);
});
test('the helpers read and write exactly those fields', () => {
const view = { _commandExtents: [{ command: {} }] };
assert.equal(defaultView({ _defaultView: view }), view);
assert.equal(
pickOffscreenView({ _picking: { _pickOffscreenView: view } }),
view,
);
assert.equal(commandExtents(view), view._commandExtents);
assert.deepEqual(commandExtents(undefined), []);
const collection = {};
assert.equal(
billboardCollectionOf({ _billboardCollection: collection }),
collection,
);
assert.equal(
clusterPointCollection({ _clusterPointCollection: collection }),
collection,
);
assert.equal(clusterPointCollection(undefined), null);
const widget = { _renderLoopRunning: true };
closeRenderLoopLatch({ _cesiumWidget: widget });
assert.equal(widget._renderLoopRunning, false);
closeRenderLoopLatch({});
const gl = {};
assert.equal(webglContext({ context: { _gl: gl } }), gl);
assert.equal(cameraFlightActive({ _currentFlight: {} }), true);
assert.equal(cameraFlightActive({ _currentFlight: undefined }), false);
});
test('private Cesium fields are reached only through internals.js', () => {
// A private field is one Cesium assigns as `this._name`. Core's own objects
// use underscore fields too, and some share a name with one of Cesium's,
// so a name Core itself defines (on `this`, a state object or as a key) is
// Core's; any other `._name` from Cesium's set is a private Cesium read.
const walk = (dir, filter) =>
fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return walk(full, filter);
return filter(entry.name) ? [full] : [];
});
const cesiumPrivate = new Set();
for (const file of [
...walk(ENGINE, (name) => name.endsWith('.js')),
...walk(WIDGETS, (name) => name.endsWith('.js')),
])
for (const [, name] of fs
.readFileSync(file, 'utf8')
.matchAll(/this\._([A-Za-z]\w*)/g))
cesiumPrivate.add(name);
const INTERNALS = 'src/render/cesium/internals.js';
const sources = walk(path.join(ROOT, 'src'), (name) =>
/\.(js|vue)$/.test(name),
)
.map((file) => path.relative(ROOT, file))
.filter((file) => file !== INTERNALS)
.map((file) => [
file,
fs
.readFileSync(path.join(ROOT, file), 'utf8')
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/^\s*\/\/.*$/gm, ''),
]);
const coreOwn = new Set();
const definitions = [
/\b(?:this|\w*[sS]tate|owner|self|parts|anno|entry|record|rec|cal|label|controller)\._([A-Za-z]\w*)\s*=(?!=)/g,
/^\s*(?:static\s+)?(?:async\s+)?_([A-Za-z]\w*)\s*[(=:;]/gm,
/[{,]\s*_([A-Za-z]\w*)\s*:/g,
];
for (const [, code] of sources)
for (const pattern of definitions)
for (const [, name] of code.matchAll(pattern)) coreOwn.add(name);
// A Cesium object's private field is flagged whatever Core names its own
// fields: on a receiver that is a Cesium object, or by bracket access.
const CESIUM_RECEIVER =
/\b_?(?:viewer|scene|camera|globe|context|frameState|clustering|tileset|imageryProvider|terrainProvider|screenSpaceCameraController|cesiumWidget|imageryLayers|primitives|dataSources|entities|clock)\??\._([A-Za-z]\w*)\b/g;
const BRACKET = /\[\s*['"`]_([A-Za-z]\w*)['"`]\s*\]/g;
const offenders = [];
for (const [file, code] of sources) {
for (const [, name] of code.matchAll(/\._([A-Za-z]\w*)\b/g))
if (cesiumPrivate.has(name) && !coreOwn.has(name))
offenders.push(`${file} ._${name}`);
for (const pattern of [CESIUM_RECEIVER, BRACKET])
for (const [, name] of code.matchAll(pattern))
if (cesiumPrivate.has(name)) offenders.push(`${file} ._${name}`);
}
assert.deepEqual([...new Set(offenders)], []);
});
+50
View File
@@ -0,0 +1,50 @@
/**
* @module render/cesium/tileRequests
* @description Admission hooks for an imagery provider's tile requests.
*
* A layer that rations its tiles (a request budget, readiness accounting,
* dropping a frame that is no longer shown) states that policy as a hook
* instead of replacing the provider's `requestImage`. The provider itself is
* left untouched: the returned provider delegates everything to it through
* its prototype and routes only tile requests, and optionally the credit,
* through the hook.
*
* Fields Cesium writes onto a provider land on the returned one, so a
* provider that reads such a field itself (a time-dynamic WMTS provider's
* `_reload`) is not suited; the URL-template and raster providers that use
* hooks today read none.
*/
/**
* @typedef {object} TileRequest
* @property {number} x
* @property {number} y
* @property {number} level
* @property {object} [request] Cesium `Request`, when Cesium passed one.
*/
/**
* @param {object} provider A Cesium imagery provider (or one built by Core).
* @param {object} hooks
* @param {(tile: TileRequest, issue: () => Promise<any>|undefined) => Promise<any>|undefined} [hooks.requestImage]
* Decides each request. `issue()` asks the provider; returning `undefined`
* defers the tile, and Cesium asks again on a later frame.
* @param {() => object} [hooks.credit] The credit Cesium shows for the layer,
* read every frame, so it can change without restaging the imagery.
* @returns {object} A provider to hand to `ImageryLayer`.
*/
export function withTileRequestHook(provider, { requestImage, credit } = {}) {
const hooked = Object.create(provider);
if (requestImage)
hooked.requestImage = (x, y, level, request) =>
requestImage({ x, y, level, request }, () =>
provider.requestImage(x, y, level, request),
);
if (credit)
Object.defineProperty(hooked, 'credit', {
configurable: true,
enumerable: true,
get: credit,
});
return hooked;
}
+68
View File
@@ -0,0 +1,68 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { withTileRequestHook } from './tileRequests.js';
class Provider {
constructor() {
this._tilingScheme = { name: 'geographic' };
this.calls = [];
}
get tilingScheme() {
return this._tilingScheme;
}
get credit() {
return 'stock';
}
requestImage(x, y, level, request) {
this.calls.push([x, y, level, request]);
return Promise.resolve({ x, y, level });
}
}
test('the hook decides each tile request and issues it on the provider', async () => {
const provider = new Provider();
const seen = [];
const hooked = withTileRequestHook(provider, {
requestImage: (tile, issue) => {
seen.push(tile);
return tile.level > 3 ? undefined : issue();
},
});
const request = {};
assert.deepEqual(await hooked.requestImage(1, 2, 3, request), {
x: 1,
y: 2,
level: 3,
});
assert.equal(hooked.requestImage(1, 2, 4), undefined, 'deferred');
assert.deepEqual(seen, [
{ x: 1, y: 2, level: 3, request },
{ x: 1, y: 2, level: 4, request: undefined },
]);
assert.deepEqual(provider.calls, [[1, 2, 3, request]]);
});
test('the provider itself is left untouched and still read through', () => {
const provider = new Provider();
const own = Object.getOwnPropertyNames(provider);
let credit = 'first';
const hooked = withTileRequestHook(provider, {
requestImage: (_tile, issue) => issue(),
credit: () => credit,
});
assert.deepEqual(Object.getOwnPropertyNames(provider), own);
assert.equal(provider.requestImage, Provider.prototype.requestImage);
assert.equal(provider.credit, 'stock');
assert.equal(hooked.tilingScheme, provider.tilingScheme);
assert.equal(hooked.credit, 'first');
credit = 'refreshed';
assert.equal(hooked.credit, 'refreshed', 'read every time');
});
test('without hooks the provider answers as it is', () => {
const provider = new Provider();
const hooked = withTileRequestHook(provider);
hooked.requestImage(0, 0, 0);
assert.equal(provider.calls.length, 1);
assert.equal(hooked.credit, 'stock');
});
+9 -4
View File
@@ -24,6 +24,12 @@
* tail itself this module can go.
*/
import {
commandExtents,
defaultView,
pickOffscreenView,
} from './render/cesium/internals.js';
/** Drop command references a view no longer draws. */
function trimView(view, { keepCurrent }) {
for (const frustum of view?.frustumCommandsList || []) {
@@ -35,8 +41,7 @@ function trimView(view, { keepCurrent }) {
}
}
if (!keepCurrent)
for (const extent of view?._commandExtents || [])
extent.command = undefined;
for (const extent of commandExtents(view)) extent.command = undefined;
}
/**
@@ -45,8 +50,8 @@ function trimView(view, { keepCurrent }) {
* @param {object} scene Cesium Scene, right after a render.
*/
export function trimStaleSceneCommands(scene) {
trimView(scene._defaultView, { keepCurrent: true });
trimView(scene._picking?._pickOffscreenView, { keepCurrent: false });
trimView(defaultView(scene), { keepCurrent: true });
trimView(pickOffscreenView(scene), { keepCurrent: false });
}
/**