Load the Cesium SDK from index.html again, since engine-neutral modules read its global when they load

This reverts ae13a56f. In the production build every 'cesium' import is the global that Cesium.js defines; with the script loaded only when a Cesium globe started, modules evaluated at boot (cesiumConstants, annotationEngine) threw ReferenceError and neither engine started. A cold Navara start loading Cesium.js stays a known gap.
This commit is contained in:
German Khokhlov committed 2026-11-03 23:21:00 +04:00
1 parent 9cc108835e
commit 125fbc5a3b
27 files changed
+119 -269

No files matched your search

+1 -3
View File
@@ -186,9 +186,7 @@ export function createBrowserViteConfig({
apiResponseHeadersPlugin(), apiResponseHeadersPlugin(),
// Next, so no provider route answers a Host the server does not allow. // Next, so no provider route answers a Host the server does not allow.
hostCheckPlugin(), hostCheckPlugin(),
// Keep the SDK asset serving/copying and external imports, but let the cesium(),
// scene load the script and stylesheet only when Cesium is requested.
{ ...cesium(), transformIndexHtml: undefined },
// Application views are Vue single-file components. // Application views are Vue single-file components.
vue(VUE_PLUGIN_OPTIONS), vue(VUE_PLUGIN_OPTIONS),
...plugins, ...plugins,
-2
View File
@@ -1984,7 +1984,6 @@
"traffic-layer": { "traffic-layer": {
"exports": ["./layers/traffic"], "exports": ["./layers/traffic"],
"modules": [ "modules": [
"src/layers/shared/color.js",
"src/data/cameraSensitivity.js", "src/data/cameraSensitivity.js",
"src/data/flowMatch.js", "src/data/flowMatch.js",
"src/data/tomtomTiles.js", "src/data/tomtomTiles.js",
@@ -2658,7 +2657,6 @@
"src/app/layers/traffic.js", "src/app/layers/traffic.js",
"src/app/operations.js", "src/app/operations.js",
"src/app/scene.js", "src/app/scene.js",
"src/app/loadCesium.js",
"src/sources/sourceSlot.js", "src/sources/sourceSlot.js",
"src/app/sources.js", "src/app/sources.js",
"src/app/startupChrome.js", "src/app/startupChrome.js",
+2 -2
View File
@@ -46,10 +46,10 @@ const PALETTE = {
* *
* BOTH paints onto terrain AND onto 3D tiles, so one value covers both worlds. * BOTH paints onto terrain AND onto 3D tiles, so one value covers both worlds.
*/ */
const CLASSIFY = Cesium.ClassificationType.BOTH;
const CLAMP = Cesium.HeightReference.CLAMP_TO_GROUND;
export function createWorldAnnotationRenderer(viewer) { export function createWorldAnnotationRenderer(viewer) {
const CLASSIFY = Cesium.ClassificationType.BOTH;
const CLAMP = Cesium.HeightReference.CLAMP_TO_GROUND;
const dataSource = new Cesium.CustomDataSource('gev-annotations'); const dataSource = new Cesium.CustomDataSource('gev-annotations');
viewer.dataSources.add(dataSource); viewer.dataSources.add(dataSource);
const osmAnnotations = new Set(); const osmAnnotations = new Set();
-52
View File
@@ -1,52 +0,0 @@
// The production build externalizes Cesium; the dev server imports its ESM
// package. Both load widget CSS only when a native viewer is requested.
export function createCesiumLoader({
document = globalThis.document,
globals = globalThis,
base = `${import.meta.env?.BASE_URL ?? '/'}cesium/`,
script = import.meta.env?.PROD === true,
} = {}) {
let pending;
return () => {
if (pending) return pending;
globals.CESIUM_BASE_URL ??= base;
const root = globals.CESIUM_BASE_URL.replace(/\/?$/, '/');
const elements = [];
const load = (tag, url) =>
new Promise((resolve, reject) => {
const element = document.createElement(tag);
elements.push(element);
if (tag === 'script') element.src = url;
else {
element.rel = 'stylesheet';
element.href = url;
}
element.onload = () => {
element.onload = element.onerror = null;
resolve();
};
element.onerror = () => reject(new Error(`Could not load ${url}`));
document.head.append(element);
});
pending = Promise.all([
load('link', `${root}Widgets/widgets.css`),
...(script && !globals.Cesium
? [load('script', `${root}Cesium.js`)]
: []),
]).catch((error) => {
for (const element of elements) {
element.onload = element.onerror = null;
element.remove();
}
pending = null;
throw error;
});
return pending;
};
}
let load;
export function loadCesium() {
load ??= createCesiumLoader();
return load();
}
-84
View File
@@ -1,84 +0,0 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { spawnSync } from 'node:child_process';
import { createCesiumLoader } from './loadCesium.js';
function fixture(globals = {}) {
const elements = [];
const document = {
createElement: (tag) => ({
tag,
remove() {
elements.splice(elements.indexOf(this), 1);
},
}),
head: { append: (element) => elements.push(element) },
};
return {
elements,
globals,
load: createCesiumLoader({
document,
globals,
base: '/app/cesium/',
script: true,
}),
};
}
test('Cesium assets load together only on demand and are shared across starts', async () => {
const { elements, globals, load } = fixture();
assert.equal(elements.length, 0);
const first = load();
assert.equal(load(), first);
assert.equal(globals.CESIUM_BASE_URL, '/app/cesium/');
assert.deepEqual(
elements.map((e) => e.href ?? e.src),
['/app/cesium/Widgets/widgets.css', '/app/cesium/Cesium.js'],
);
elements.forEach((e) => e.onload());
await first;
await load();
assert.equal(elements.length, 2);
});
test('a failed Cesium asset load can be retried and preserves the configured base', async () => {
const { elements, globals, load } = fixture({ CESIUM_BASE_URL: '/custom/' });
const failed = load();
elements[1].onerror();
await assert.rejects(failed, /Could not load/);
assert.equal(elements.length, 0);
const retried = load();
assert.equal(globals.CESIUM_BASE_URL, '/custom/');
assert.equal(elements[1].src, '/custom/Cesium.js');
elements.forEach((e) => e.onload());
await retried;
});
test('the standalone startup graph can be imported before the Cesium SDK exists', () => {
// Empty SDK exports make any boot-time native constructor or enum read fail.
// mgrs is CJS in Node; its browser named export is unrelated to this gate.
const script = `
import { registerHooks } from 'node:module';
registerHooks({ resolve(specifier, context, next) {
const code = specifier === 'cesium'
? 'export const Axis=undefined,ScreenSpaceEventType=undefined,BoundingSphere=undefined,Color=undefined,PostProcessStage=undefined;'
: specifier === 'mgrs' ? 'export function forward() {}' : null;
return code ? { url: 'data:text/javascript,' + encodeURIComponent(code), shortCircuit: true } : next(specifier, context);
}});
await import('./src/standalone/application.js');
const { registerNavaraLayerRenderers } = await import('./src/app/navaraMode.js');
await registerNavaraLayerRenderers();
const { createStandaloneCatalog } = await import('./src/standalone/catalog.js');
const lifetime = new AbortController();
const catalog = createStandaloneCatalog({ engineKind: 'navara', signal: lifetime.signal });
if (catalog.layers.length !== 33) throw new Error('Incomplete cold Navara catalog');
lifetime.abort();
`;
const result = spawnSync(
process.execPath,
['--import=./build/registerVue.js', '--input-type=module', '-e', script],
{ encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
});
-3
View File
@@ -17,7 +17,6 @@ import {
import { describeError } from './errors.js'; import { describeError } from './errors.js';
import { createSceneEngine } from './globeEngine.js'; import { createSceneEngine } from './globeEngine.js';
import { onLocaleChange, t } from '../i18n/index.js'; import { onLocaleChange, t } from '../i18n/index.js';
import { loadCesium } from './loadCesium.js';
/** /**
* Construct the application globe using the caller's local configuration. * Construct the application globe using the caller's local configuration.
@@ -140,8 +139,6 @@ async function createRenderer({
} }
if (engineKind !== 'cesium') if (engineKind !== 'cesium')
throw new TypeError(`Unknown globe engine: ${engineKind}`); throw new TypeError(`Unknown globe engine: ${engineKind}`);
await loadCesium();
signal.throwIfAborted();
loaderStatus.textContent = t('Configuring viewer...'); loaderStatus.textContent = t('Configuring viewer...');
// Provider attribution stays visible, including clean-view and recording. // Provider attribution stays visible, including clean-view and recording.
const creditContainer = document.createElement('div'); const creditContainer = document.createElement('div');
+1 -3
View File
@@ -1,6 +1,4 @@
import { LayerBindings } from './ui/layerBindings.js'; import { LayerBindings } from './ui/layerBindings.js';
import { ScreenSpaceEventType } from 'cesium';
import { FIRMS_LEFT_CLICK } from './layers/firms/render/cesium.js';
import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs'; import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs';
import { readLayerSource } from './testSupport/readLayerSource.mjs'; import { readLayerSource } from './testSupport/readLayerSource.mjs';
import { enter as cockpitEnter, navigateContext } from './ui/cockpitTrackingController.js'; import { enter as cockpitEnter, navigateContext } from './ui/cockpitTrackingController.js';
@@ -418,7 +416,7 @@ test('vessel and fire layers announce valid clicks and never fly cameras', () =>
); );
assert.match(vesselFocus, /requestWorldFocus\(\{/); assert.match(vesselFocus, /requestWorldFocus\(\{/);
assert.match(firms, /const clickType\s*=\s*layerState\._renderer\?\.clickType\s*\?\?\s*config\.leftClickEventType/); assert.match(firms, /const clickType\s*=\s*layerState\._renderer\?\.clickType\s*\?\?\s*config\.leftClickEventType/);
assert.equal(FIRMS_LEFT_CLICK, ScreenSpaceEventType.LEFT_CLICK); assert.match(firms, /FIRMS_LEFT_CLICK = Cesium\.ScreenSpaceEventType\.LEFT_CLICK/);
const fireClick = body( const fireClick = body(
firms, firms,
/_clickHandler\.setInputAction\(\(click\) => \{([\s\S]*?)\n \}, clickType\);/, /_clickHandler\.setInputAction\(\(click\) => \{([\s\S]*?)\n \}, clickType\);/,
+8 -9
View File
@@ -9,7 +9,14 @@ export const CYBER_SONAR_CESIUM_VERSION = '1.138.0';
// Follow every public DrawCommand setter, including pick/shadow/culling flags. // Follow every public DrawCommand setter, including pick/shadow/culling flags.
// Copying native.dirty would keep derived pipelines dirty forever: the scene // Copying native.dirty would keep derived pipelines dirty forever: the scene
// consumes our command, so it never clears that flag on the native command. // consumes our command, so it never clears that flag on the native command.
let commandProperties; const commandProperties = Object.entries(
Object.getOwnPropertyDescriptors(Cesium.DrawCommand.prototype),
)
.filter(
([key, descriptor]) =>
descriptor.set && key !== 'shaderProgram' && key !== 'uniformMap',
)
.map(([key]) => key);
export function syncCyberSonarCommand( export function syncCyberSonarCommand(
native, native,
@@ -18,14 +25,6 @@ export function syncCyberSonarCommand(
uniforms, uniforms,
labelBackground, labelBackground,
) { ) {
commandProperties ??= Object.entries(
Object.getOwnPropertyDescriptors(Cesium.DrawCommand.prototype),
)
.filter(
([key, descriptor]) =>
descriptor.set && key !== 'shaderProgram' && key !== 'uniformMap',
)
.map(([key]) => key);
if (!record.command) { if (!record.command) {
record.command = new Cesium.DrawCommand(); record.command = new Cesium.DrawCommand();
record.uniformMap = { record.uniformMap = {
+7 -8
View File
@@ -47,9 +47,8 @@ const SCENE_PATH_RENDER_HOLD = 'bhote-koshi-scene-path-travel';
const SCENE_MEDIA_MIN_PLAYBACK_SECONDS = 6; const SCENE_MEDIA_MIN_PLAYBACK_SECONDS = 6;
const SCENE_MEDIA_EXIT_SECONDS = 0.65; const SCENE_MEDIA_EXIT_SECONDS = 0.65;
const BHOTE_EVENT_ACCENT = WORLD_OVERLAY_STYLE.accent; const BHOTE_EVENT_ACCENT = WORLD_OVERLAY_STYLE.accent;
const FLOOD_COLOR = () => const FLOOD_COLOR = Cesium.Color.fromCssColorString('#d8954e').withAlpha(0.68);
Cesium.Color.fromCssColorString('#d8954e').withAlpha(0.68); const SURGE_COLOR = Cesium.Color.fromCssColorString('#e8b47c');
const SURGE_COLOR = () => Cesium.Color.fromCssColorString('#e8b47c');
const CINEMATIC_INPUT_EVENTS = Object.freeze(['pointerdown', 'wheel']); const CINEMATIC_INPUT_EVENTS = Object.freeze(['pointerdown', 'wheel']);
const CINEMATIC_VIEW_SPECS = Object.freeze([ const CINEMATIC_VIEW_SPECS = Object.freeze([
{ {
@@ -1078,8 +1077,8 @@ export function createBhoteKoshiEventLayer({
let _cinematicKeyframes = []; let _cinematicKeyframes = [];
let _cinematicActive = false; let _cinematicActive = false;
let _cinematicReplayArmed = false; let _cinematicReplayArmed = false;
let _cinematicPose = { target: { x: 0, y: 0, z: 0 } }; let _cinematicPose = { target: new Cesium.Cartesian3() };
let _cinematicOffset = { heading: 0, pitch: 0, range: 0 }; let _cinematicOffset = new Cesium.HeadingPitchRange();
let _lastCinematicProgress = null; let _lastCinematicProgress = null;
let _cameraInputTarget = null; let _cameraInputTarget = null;
let _evidenceClickTarget = null; let _evidenceClickTarget = null;
@@ -2571,11 +2570,11 @@ export function createBhoteKoshiEventLayer({
const tileSafe = usesPhotorealFloodSurface(); const tileSafe = usesPhotorealFloodSurface();
_floodSurfaceMode = tileSafe ? 'tiles' : 'terrain'; _floodSurfaceMode = tileSafe ? 'tiles' : 'terrain';
const haloMaterial = new Cesium.PolylineDashMaterialProperty({ const haloMaterial = new Cesium.PolylineDashMaterialProperty({
color: FLOOD_COLOR().withAlpha(0.38), color: FLOOD_COLOR.withAlpha(0.38),
dashLength: 18, dashLength: 18,
}); });
const coreMaterial = new Cesium.PolylineGlowMaterialProperty({ const coreMaterial = new Cesium.PolylineGlowMaterialProperty({
color: FLOOD_COLOR().withAlpha(0.96), color: FLOOD_COLOR.withAlpha(0.96),
glowPower: 0.24, glowPower: 0.24,
taperPower: 0.7, taperPower: 0.7,
}); });
@@ -2620,7 +2619,7 @@ export function createBhoteKoshiEventLayer({
position: Cesium.Cartesian3.ZERO, position: Cesium.Cartesian3.ZERO,
point: { point: {
pixelSize: 11, pixelSize: 11,
color: SURGE_COLOR(), color: SURGE_COLOR,
outlineColor: Cesium.Color.BLACK.withAlpha(0.85), outlineColor: Cesium.Color.BLACK.withAlpha(0.85),
outlineWidth: 3, outlineWidth: 3,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+12 -12
View File
@@ -144,10 +144,10 @@ const NEARBY_CITIES = Object.freeze([
placement: 'right-lower', placement: 'right-lower',
}), }),
]); ]);
const BORDER_COLOR = () => Cesium.Color.fromCssColorString('#20e7f2'); const BORDER_COLOR = Cesium.Color.fromCssColorString('#20e7f2');
const BORDER_ACCENT = '#20e7f2'; const BORDER_ACCENT = '#20e7f2';
const INCIDENT_ACCENT = '#ffad55'; const INCIDENT_ACCENT = '#ffad55';
const INCIDENT_COLOR = () => Cesium.Color.fromCssColorString(INCIDENT_ACCENT); const INCIDENT_COLOR = Cesium.Color.fromCssColorString(INCIDENT_ACCENT);
const DEFAULT_OVERLAY_HOST = Object.freeze({ const DEFAULT_OVERLAY_HOST = Object.freeze({
setEntries: setOverlayEntries, setEntries: setOverlayEntries,
@@ -521,7 +521,7 @@ export function createBhoteKoshiLocatorLayer({
sequenceStartedAt, sequenceStartedAt,
visibleAfterMs = 0, visibleAfterMs = 0,
) { ) {
addCallout(dataSource, INCIDENT, INCIDENT_COLOR(), { addCallout(dataSource, INCIDENT, INCIDENT_COLOR, {
sequenceStartedAt, sequenceStartedAt,
visibleAfterMs, visibleAfterMs,
}); });
@@ -555,12 +555,12 @@ export function createBhoteKoshiLocatorLayer({
false, false,
), ),
color: new Cesium.CallbackProperty((_time, result) => { color: new Cesium.CallbackProperty((_time, result) => {
const color = Cesium.Color.clone(INCIDENT_COLOR(), result); const color = Cesium.Color.clone(INCIDENT_COLOR, result);
color.alpha = 0.16 * (1 - pulseProgress()); color.alpha = 0.16 * (1 - pulseProgress());
return color; return color;
}, false), }, false),
outlineColor: new Cesium.CallbackProperty((_time, result) => { outlineColor: new Cesium.CallbackProperty((_time, result) => {
const color = Cesium.Color.clone(INCIDENT_COLOR(), result); const color = Cesium.Color.clone(INCIDENT_COLOR, result);
color.alpha = isVisible() ? 0.08 + 0.78 * (1 - pulseProgress()) : 0; color.alpha = isVisible() ? 0.08 + 0.78 * (1 - pulseProgress()) : 0;
return color; return color;
}, false), }, false),
@@ -705,7 +705,7 @@ export function createBhoteKoshiLocatorLayer({
labelPlacement = 'right', labelPlacement = 'right',
{ showAnchor = true } = {}, { showAnchor = true } = {},
) { ) {
addCallout(dataSource, NEPAL, BORDER_COLOR(), { addCallout(dataSource, NEPAL, BORDER_COLOR, {
labelPlacement, labelPlacement,
sequenceStartedAt, sequenceStartedAt,
showAnchor, showAnchor,
@@ -820,12 +820,12 @@ export function createBhoteKoshiLocatorLayer({
false, false,
), ),
color: new Cesium.CallbackProperty((_time, result) => { color: new Cesium.CallbackProperty((_time, result) => {
const color = Cesium.Color.clone(INCIDENT_COLOR(), result); const color = Cesium.Color.clone(INCIDENT_COLOR, result);
color.alpha = 0.16 * (1 - pulseProgress()); color.alpha = 0.16 * (1 - pulseProgress());
return color; return color;
}, false), }, false),
outlineColor: new Cesium.CallbackProperty((_time, result) => { outlineColor: new Cesium.CallbackProperty((_time, result) => {
const color = Cesium.Color.clone(INCIDENT_COLOR(), result); const color = Cesium.Color.clone(INCIDENT_COLOR, result);
color.alpha = 0.08 + 0.78 * (1 - pulseProgress()); color.alpha = 0.08 + 0.78 * (1 - pulseProgress());
return color; return color;
}, false), }, false),
@@ -890,7 +890,7 @@ export function createBhoteKoshiLocatorLayer({
positions: animatedPositions, positions: animatedPositions,
width: 3.5, width: 3.5,
clampToGround: true, clampToGround: true,
material: BORDER_COLOR().withAlpha(0.96), material: BORDER_COLOR.withAlpha(0.96),
classificationType: Cesium.ClassificationType.BOTH, classificationType: Cesium.ClassificationType.BOTH,
}, },
}); });
@@ -915,7 +915,7 @@ export function createBhoteKoshiLocatorLayer({
show: new Cesium.CallbackProperty(() => now() >= drawStartedAt, false), show: new Cesium.CallbackProperty(() => now() >= drawStartedAt, false),
polygon: { polygon: {
hierarchy, hierarchy,
material: BORDER_COLOR().withAlpha(0.055), material: BORDER_COLOR.withAlpha(0.055),
classificationType: Cesium.ClassificationType.BOTH, classificationType: Cesium.ClassificationType.BOTH,
}, },
}); });
@@ -925,7 +925,7 @@ export function createBhoteKoshiLocatorLayer({
positions: animatedPositions, positions: animatedPositions,
width: 5, width: 5,
clampToGround: true, clampToGround: true,
material: BORDER_COLOR().withAlpha(0.12), material: BORDER_COLOR.withAlpha(0.12),
classificationType: Cesium.ClassificationType.BOTH, classificationType: Cesium.ClassificationType.BOTH,
}, },
}); });
@@ -935,7 +935,7 @@ export function createBhoteKoshiLocatorLayer({
positions: animatedPositions, positions: animatedPositions,
width: 2.5, width: 2.5,
clampToGround: true, clampToGround: true,
material: BORDER_COLOR().withAlpha(0.96), material: BORDER_COLOR.withAlpha(0.96),
classificationType: Cesium.ClassificationType.BOTH, classificationType: Cesium.ClassificationType.BOTH,
}, },
}); });
+4 -4
View File
@@ -21,17 +21,17 @@ const STYLE = [
'imageSubRegion', 'imageSubRegion',
]; ];
/** Own static imported pins without a per-frame Entity visualizer walk. */ class ImportedBillboards extends Cesium.BillboardCollection {
export function createImportedPins(dataSource, viewer) {
class ImportedBillboards extends Cesium.BillboardCollection {
update(frameState) { update(frameState) {
// Height queries should find the map, never another imported marker. // Height queries should find the map, never another imported marker.
// Ordinary click picks and rendered screenshots still include the pins. // Ordinary click picks and rendered screenshots still include the pins.
if (frameState.passes.pick && frameState.passes.offscreen) return; if (frameState.passes.pick && frameState.passes.offscreen) return;
super.update(frameState); super.update(frameState);
} }
} }
/** Own static imported pins without a per-frame Entity visualizer walk. */
export function createImportedPins(dataSource, viewer) {
const all = dataSource.entities.values.slice(); const all = dataSource.entities.values.slice();
const now = Cesium.JulianDate.now(); const now = Cesium.JulianDate.now();
const pins = all.filter((entity) => entity.billboard && entity.position); const pins = all.filter((entity) => entity.billboard && entity.position);
+12 -3
View File
@@ -1,3 +1,4 @@
import { modelAxis } from './render/cesiumConstants.js';
import * as geo from '../geo/index.js'; import * as geo from '../geo/index.js';
/** /**
* Model-space visual centres measured from the shipped GLBs' scene-space AABBs. * Model-space visual centres measured from the shipped GLBs' scene-space AABBs.
@@ -70,9 +71,17 @@ export const MODEL_TRAIL_ANCHOR_NATIVE = Object.freeze({
'/models/atr72.glb': Object.freeze([12.9785, -1.2935, 0]), '/models/atr72.glb': Object.freeze([12.9785, -1.2935, 0]),
}); });
// glTF Y-up/Z-forward to the model frame: Cesium's Y_UP_TO_Z_UP × /** @constant Cesium's OWN axis correction for a glTF loaded with the
// Z_UP_TO_X_UP, shared with Navara before the Cesium SDK is loaded. * `Model.fromGltfAsync` defaults (upAxis Y, forwardAxis Z), assembled from its
const AXIS_CORRECTION = [0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]; * exported constants rather than restated as numbers here. `ModelSceneGraph`
* renders with `modelMatrix x components.transform x axisCorrection`, and the
* trail anchor has to ride the same chain or it lands on a different axis than
* the aircraft it is supposed to be attached to. */
const AXIS_CORRECTION = geo.matrix4.multiplyTransformation(
modelAxis.Y_UP_TO_Z_UP,
modelAxis.Z_UP_TO_X_UP,
geo.matrix4.create(),
);
const _anchorScratch = geo.cartesian3.create(); const _anchorScratch = geo.cartesian3.create();
const _chainScratch = geo.matrix4.create(); const _chainScratch = geo.matrix4.create();
+3 -4
View File
@@ -6,12 +6,11 @@ export function worldToWindowCoordinates(scene, position, result) {
result, result,
); );
} }
let occluder; const occluder = new Cesium.EllipsoidalOccluder(
export function horizonOccluder(camera) {
occluder ??= new Cesium.EllipsoidalOccluder(
Cesium.Ellipsoid.WGS84, Cesium.Ellipsoid.WGS84,
new Cesium.Cartesian3(), new Cesium.Cartesian3(),
); );
export function horizonOccluder(camera) {
occluder.cameraPosition = camera.positionWC; occluder.cameraPosition = camera.positionWC;
return occluder; return occluder;
} }
+3 -3
View File
@@ -9,11 +9,11 @@ import { toCesium2, toCesium3, toGeo3 } from './convert.js';
import { createCesiumFollow } from './follow.js'; import { createCesiumFollow } from './follow.js';
const { math } = geo; const { math } = geo;
const easings = () => ({ const EASINGS = {
cubicInOut: Cesium.EasingFunction.CUBIC_IN_OUT, cubicInOut: Cesium.EasingFunction.CUBIC_IN_OUT,
sinusoidalInOut: Cesium.EasingFunction.SINUSOIDAL_IN_OUT, sinusoidalInOut: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
linear: Cesium.EasingFunction.LINEAR_NONE, linear: Cesium.EasingFunction.LINEAR_NONE,
}); };
// Cesium reports heading and roll in [0, 2π]; the contract wants [0, 2π). // Cesium reports heading and roll in [0, 2π]; the contract wants [0, 2π).
function wrapAngle(angle) { function wrapAngle(angle) {
@@ -43,7 +43,7 @@ const cesiumDestination = (destination) =>
) )
: toCesium3(destination); : toCesium3(destination);
const cesiumEasing = (easing) => (easing ? easings()[easing] : undefined); const cesiumEasing = (easing) => (easing ? EASINGS[easing] : undefined);
// A HeadingPitchRange or a Cartesian offset, as Cesium's lookAt takes them. // A HeadingPitchRange or a Cartesian offset, as Cesium's lookAt takes them.
const cesiumOffset = (offset) => const cesiumOffset = (offset) =>
+1 -2
View File
@@ -4,7 +4,7 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { bridgeEvent } from './events.js'; import { bridgeEvent } from './events.js';
let scratchGregorian; const scratchGregorian = new Cesium.GregorianDate();
// JulianDate.toDate(time).getTime() without the Date: frame events must not // JulianDate.toDate(time).getTime() without the Date: frame events must not
// allocate. A raise without a time (not Cesium's own) reads the clock. // allocate. A raise without a time (not Cesium's own) reads the clock.
@@ -27,7 +27,6 @@ function unixMilliseconds(scene, time) {
* @returns {{ frames: object, destroy: () => void }} * @returns {{ frames: object, destroy: () => void }}
*/ */
export function createCesiumFrames(viewer) { export function createCesiumFrames(viewer) {
scratchGregorian ??= new Cesium.GregorianDate();
const { scene } = viewer; const { scene } = viewer;
const bridges = ['preUpdate', 'preRender', 'postRender'].map((name) => [ const bridges = ['preUpdate', 'preRender', 'postRender'].map((name) => [
name, name,
+13 -13
View File
@@ -6,6 +6,19 @@ import { normalizeInputOptions } from '../contract.js';
import { createPickDelivery } from '../pickDelivery.js'; import { createPickDelivery } from '../pickDelivery.js';
import { toGeo2 } from './convert.js'; import { toGeo2 } from './convert.js';
const TYPES = {
click: Cesium.ScreenSpaceEventType.LEFT_CLICK,
doubleClick: Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK,
move: Cesium.ScreenSpaceEventType.MOUSE_MOVE,
down: Cesium.ScreenSpaceEventType.LEFT_DOWN,
up: Cesium.ScreenSpaceEventType.LEFT_UP,
};
const KEYS = {
ctrl: Cesium.KeyboardEventModifier.CTRL,
shift: Cesium.KeyboardEventModifier.SHIFT,
alt: Cesium.KeyboardEventModifier.ALT,
};
// Cesium reuses its event objects; listeners get geo copies. // Cesium reuses its event objects; listeners get geo copies.
const toEvent = (event) => { const toEvent = (event) => {
const result = {}; const result = {};
@@ -31,19 +44,6 @@ export function createCesiumInput(
scheduleFrame = (run) => requestAnimationFrame(run), scheduleFrame = (run) => requestAnimationFrame(run),
} = {}, } = {},
) { ) {
const TYPES = {
click: Cesium.ScreenSpaceEventType.LEFT_CLICK,
doubleClick: Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK,
move: Cesium.ScreenSpaceEventType.MOUSE_MOVE,
down: Cesium.ScreenSpaceEventType.LEFT_DOWN,
up: Cesium.ScreenSpaceEventType.LEFT_UP,
};
const KEYS = {
ctrl: Cesium.KeyboardEventModifier.CTRL,
shift: Cesium.KeyboardEventModifier.SHIFT,
alt: Cesium.KeyboardEventModifier.ALT,
};
const { scene } = viewer; const { scene } = viewer;
let handler = null; let handler = null;
let destroyed = false; let destroyed = false;
+5 -10
View File
@@ -8,12 +8,12 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { createMoverRegistry } from '../movers.js'; import { createMoverRegistry } from '../movers.js';
let scratchPosition; const scratchPosition = new Cesium.Cartesian3();
let scratchColor; const scratchColor = new Cesium.Color();
let scratchQuaternion; const scratchQuaternion = new Cesium.Quaternion();
let scratchRotation; const scratchRotation = new Cesium.Matrix3();
// Model.modelMatrix and Model.color copy what they are given. // Model.modelMatrix and Model.color copy what they are given.
let scratchModelMatrix; const scratchModelMatrix = new Cesium.Matrix4();
/** The atlas cells as canvases, by key (a DOM canvas or image source). */ /** The atlas cells as canvases, by key (a DOM canvas or image source). */
async function cropAtlasCells(atlas, keys) { async function cropAtlasCells(atlas, keys) {
@@ -287,11 +287,6 @@ export function createCesiumMovers(
viewer, viewer,
{ requestRender = () => viewer.scene.requestRender(), factories = {} } = {}, { requestRender = () => viewer.scene.requestRender(), factories = {} } = {},
) { ) {
scratchPosition ??= new Cesium.Cartesian3();
scratchColor ??= new Cesium.Color();
scratchQuaternion ??= new Cesium.Quaternion();
scratchRotation ??= new Cesium.Matrix3();
scratchModelMatrix ??= new Cesium.Matrix4();
const make = { ...DEFAULT_FACTORIES, ...factories }; const make = { ...DEFAULT_FACTORIES, ...factories };
const { scene } = viewer; const { scene } = viewer;
let root = null; let root = null;
+8 -9
View File
@@ -68,11 +68,10 @@ export function presentRoutingFailure(error) {
/** Route colour — the annotation palette's cyan, so voice routes and Directions match. */ /** Route colour — the annotation palette's cyan, so voice routes and Directions match. */
export const DIRECTIONS_ROUTE_COLOR = '#39d0ff'; export const DIRECTIONS_ROUTE_COLOR = '#39d0ff';
const MARKER_A_COLOR = () => Cesium.Color.fromCssColorString('#5dff9f'); const MARKER_A_COLOR = Cesium.Color.fromCssColorString('#5dff9f');
const MARKER_B_COLOR = () => Cesium.Color.fromCssColorString('#ff6b6b'); const MARKER_B_COLOR = Cesium.Color.fromCssColorString('#ff6b6b');
const STEP_COLOR = () => Cesium.Color.WHITE.withAlpha(0.95); const STEP_COLOR = Cesium.Color.WHITE.withAlpha(0.95);
const STEP_OUTLINE = () => const STEP_OUTLINE = Cesium.Color.fromCssColorString(DIRECTIONS_ROUTE_COLOR);
Cesium.Color.fromCssColorString(DIRECTIONS_ROUTE_COLOR);
const STEP_PIXEL_SIZE = 8; const STEP_PIXEL_SIZE = 8;
const STEP_SELECTED_PIXEL_SIZE = 13; const STEP_SELECTED_PIXEL_SIZE = 13;
/** Route request timeout (ms) — the proxy itself gives OSRM 12 s. */ /** Route request timeout (ms) — the proxy itself gives OSRM 12 s. */
@@ -1027,10 +1026,10 @@ export function createDirectionsLayer({
if (!_viewer && !_native) return; if (!_viewer && !_native) return;
if (letter === 'a') { if (letter === 'a') {
removeEntity(_markerA); removeEntity(_markerA);
_markerA = markerEntity('A', point, MARKER_A_COLOR()); _markerA = markerEntity('A', point, MARKER_A_COLOR);
} else { } else {
removeEntity(_markerB); removeEntity(_markerB);
_markerB = markerEntity('B', point, MARKER_B_COLOR()); _markerB = markerEntity('B', point, MARKER_B_COLOR);
} }
} }
@@ -1137,9 +1136,9 @@ export function createDirectionsLayer({
_stepPoints.add({ _stepPoints.add({
id: `directions:step:${index}`, id: `directions:step:${index}`,
position: Cesium.Cartesian3.fromDegrees(step.lon, step.lat, 0), position: Cesium.Cartesian3.fromDegrees(step.lon, step.lat, 0),
color: STEP_COLOR(), color: STEP_COLOR,
pixelSize: STEP_PIXEL_SIZE, pixelSize: STEP_PIXEL_SIZE,
outlineColor: STEP_OUTLINE(), outlineColor: STEP_OUTLINE,
outlineWidth: 2, outlineWidth: 2,
show: false, show: false,
// The shared ground floor is a coarse cell, and on the photoreal stack // The shared ground floor is a coarse cell, and on the photoreal stack
+1 -2
View File
@@ -4,8 +4,7 @@ import {
horizonOccluder, horizonOccluder,
} from '../../../data/render/cesiumProjection.js'; } from '../../../data/render/cesiumProjection.js';
// Cesium ScreenSpaceEventType.LEFT_CLICK; available before the SDK loads. export const FIRMS_LEFT_CLICK = Cesium.ScreenSpaceEventType.LEFT_CLICK;
export const FIRMS_LEFT_CLICK = 2;
export const projectFirmsWindow = worldToWindowCoordinates; export const projectFirmsWindow = worldToWindowCoordinates;
export const firmsHorizonOccluder = horizonOccluder; export const firmsHorizonOccluder = horizonOccluder;
+11 -8
View File
@@ -1,6 +1,17 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { GNSS_LEVEL_COLORS } from '../records.js'; import { GNSS_LEVEL_COLORS } from '../records.js';
/**
* GroundPrimitive allowPicking:false suppresses ids but still submits stencil
* commands in Cesium's pick pass, erasing picks of overlapping ground geometry.
* This fill participates only in rendering, so sibling polygons remain pickable.
*/
class GnssGroundFill extends Cesium.GroundPrimitive {
update(frameState) {
if (!frameState.passes.pick) super.update(frameState);
}
}
/** Degrees at the centre of the current view, or null before the camera settles. */ /** Degrees at the centre of the current view, or null before the camera settles. */
export function defaultGnssViewAnchor(viewer) { export function defaultGnssViewAnchor(viewer) {
const canvas = viewer?.scene?.canvas; const canvas = viewer?.scene?.canvas;
@@ -33,14 +44,6 @@ export function createGnssCesiumRenderer({
viewer, viewer,
viewAnchor = defaultGnssViewAnchor, viewAnchor = defaultGnssViewAnchor,
}) { }) {
// GroundPrimitive allowPicking:false still submits stencil commands in
// the pick pass. Keep this fill from erasing overlapping geometry's picks.
class GnssGroundFill extends Cesium.GroundPrimitive {
update(frameState) {
if (!frameState.passes.pick) super.update(frameState);
}
}
const primitives = new Cesium.PrimitiveCollection(); const primitives = new Cesium.PrimitiveCollection();
primitives.id = 'gnss-integrity'; primitives.id = 'gnss-integrity';
primitives.show = false; primitives.show = false;
+6 -8
View File
@@ -227,7 +227,11 @@ export function viewFocus(
* (NYC −22 m), and an occluder at 0 m would hide the cones around it. * (NYC −22 m), and an occluder at 0 m would hide the cones around it.
*/ */
const HORIZON_CULL_DEPTH_M = 1000; const HORIZON_CULL_DEPTH_M = 1000;
let HORIZON_CULL_ELLIPSOID; const HORIZON_CULL_ELLIPSOID = new Cesium.Ellipsoid(
Cesium.Ellipsoid.WGS84.radii.x - HORIZON_CULL_DEPTH_M,
Cesium.Ellipsoid.WGS84.radii.y - HORIZON_CULL_DEPTH_M,
Cesium.Ellipsoid.WGS84.radii.z - HORIZON_CULL_DEPTH_M,
);
/** /**
* Hide billboards and points behind the horizon: they skip the depth test, so * Hide billboards and points behind the horizon: they skip the depth test, so
@@ -236,18 +240,12 @@ let HORIZON_CULL_ELLIPSOID;
* @param {{getViewer: () => object|null, items: () => Iterable<{position: object, show: boolean}>, onChange?: () => void}} options * @param {{getViewer: () => object|null, items: () => Iterable<{position: object, show: boolean}>, onChange?: () => void}} options
*/ */
export function createHorizonCull({ getViewer, items, onChange }) { export function createHorizonCull({ getViewer, items, onChange }) {
let occluder; const occluder = new Cesium.EllipsoidalOccluder(HORIZON_CULL_ELLIPSOID);
let from = null; let from = null;
let stopListening = null; let stopListening = null;
/** Show what is in front of the horizon, hide the rest; the count seen. */ /** Show what is in front of the horizon, hide the rest; the count seen. */
function cull(list, position) { function cull(list, position) {
HORIZON_CULL_ELLIPSOID ??= new Cesium.Ellipsoid(
Cesium.Ellipsoid.WGS84.radii.x - HORIZON_CULL_DEPTH_M,
Cesium.Ellipsoid.WGS84.radii.y - HORIZON_CULL_DEPTH_M,
Cesium.Ellipsoid.WGS84.radii.z - HORIZON_CULL_DEPTH_M,
);
occluder ??= new Cesium.EllipsoidalOccluder(HORIZON_CULL_ELLIPSOID);
occluder.cameraPosition = position; occluder.cameraPosition = position;
let seen = 0; let seen = 0;
let changed = false; let changed = false;
+2 -6
View File
@@ -86,15 +86,11 @@ export const CABLE_GLOBE_STACK_IDS = Object.freeze(
export const MARKER_COLLECTION_EXPECTATIONS = Object.freeze([ export const MARKER_COLLECTION_EXPECTATIONS = Object.freeze([
Object.freeze({ Object.freeze({
key: '_billboardCollection', key: '_billboardCollection',
get type() { type: Cesium.BillboardCollection,
return Cesium.BillboardCollection;
},
}), }),
Object.freeze({ Object.freeze({
key: '_pointCollection', key: '_pointCollection',
get type() { type: Cesium.PointPrimitiveCollection,
return Cesium.PointPrimitiveCollection;
},
}), }),
]); ]);
+3 -5
View File
@@ -68,7 +68,7 @@ export function createReticleFootprint() {
const pixels = Array.from({ length: 25 }, () => geo.cartesian2.create()); const pixels = Array.from({ length: 25 }, () => geo.cartesian2.create());
const hits = Array.from({ length: 25 }, () => geo.cartesian3.create()); const hits = Array.from({ length: 25 }, () => geo.cartesian3.create());
const carto = geo.cartographic.create(); const carto = geo.cartographic.create();
let ray; const ray = new Cesium.Ray();
return (viewer, altitude, engine) => { return (viewer, altitude, engine) => {
const camera = engine?.camera ?? viewer?.camera; const camera = engine?.camera ?? viewer?.camera;
if (!camera?.getPickRay && !camera?.pickRay) return null; if (!camera?.getPickRay && !camera?.pickRay) return null;
@@ -91,9 +91,7 @@ export function createReticleFootprint() {
p = pixels[i]; p = pixels[i];
p.x = left + (side * col) / 4; p.x = left + (side * col) / 4;
p.y = top + (side * row) / 4; p.y = top + (side * row) / 4;
const hit = engine const hit = camera.pickEllipsoid(p, Cesium.Ellipsoid.WGS84, hits[i]);
? camera.pickEllipsoid(p)
: camera.pickEllipsoid(p, Cesium.Ellipsoid.WGS84, hits[i]);
let dx, dy; let dx, dy;
if (hit) { if (hit) {
geo.cartographic.fromCartesian(hit, geo.ellipsoid.WGS84, carto); geo.cartographic.fromCartesian(hit, geo.ellipsoid.WGS84, carto);
@@ -104,7 +102,7 @@ export function createReticleFootprint() {
} else { } else {
const picked = engine const picked = engine
? camera.pickRay(p) ? camera.pickRay(p)
: camera.getPickRay(p, (ray ??= new Cesium.Ray())) || ray; : camera.getPickRay(p, ray) || ray;
if (!picked) continue; if (!picked) continue;
const d = picked.direction; const d = picked.direction;
dx = dx =
+7 -6
View File
@@ -1,4 +1,4 @@
import { rgbaFromCss } from '../shared/color.js'; import * as Cesium from 'cesium';
/** /**
* @file Street Traffic selects TomTom, OpenMapTiles or Hybrid motor roads, * @file Street Traffic selects TomTom, OpenMapTiles or Hybrid motor roads,
@@ -112,9 +112,9 @@ export const SIZE_BY_TYPE = {
*/ */
export const FLOW_BUCKET_COLORS = { export const FLOW_BUCKET_COLORS = {
free: rgbaFromCss('#2ecc71').withAlpha(0.9), free: Cesium.Color.fromCssColorString('#2ecc71').withAlpha(0.9),
slow: rgbaFromCss('#f0b23e').withAlpha(0.9), slow: Cesium.Color.fromCssColorString('#f0b23e').withAlpha(0.9),
jam: rgbaFromCss('#e05252').withAlpha(0.9), jam: Cesium.Color.fromCssColorString('#e05252').withAlpha(0.9),
}; };
// ─── Jam-viz prototype (live mode only — see 2026-07-21 design doc) ──────── // ─── Jam-viz prototype (live mode only — see 2026-07-21 design doc) ────────
@@ -140,11 +140,12 @@ export const HEAT_JAM_PULSE_ALPHA = 0.2;
/** @const {Cesium.Color} Jam corridor color (bucket red, alpha pulsed live). */ /** @const {Cesium.Color} Jam corridor color (bucket red, alpha pulsed live). */
export const HEAT_JAM_COLOR = rgbaFromCss('#e05252'); export const HEAT_JAM_COLOR = Cesium.Color.fromCssColorString('#e05252');
/** @const {Cesium.Color} Slow corridor color (bucket amber, faint + static). */ /** @const {Cesium.Color} Slow corridor color (bucket amber, faint + static). */
export const HEAT_SLOW_COLOR = rgbaFromCss('#f0b23e').withAlpha(0.2); export const HEAT_SLOW_COLOR =
Cesium.Color.fromCssColorString('#f0b23e').withAlpha(0.2);
/** @const {number} Far-distance scale floor for jam dots (shipped: 0.3). */ /** @const {number} Far-distance scale floor for jam dots (shipped: 0.3). */
+4 -4
View File
@@ -12,10 +12,10 @@ export function roadIdentity(road) {
/** Retain one road and its dot population across tile and surface publications. */ /** Retain one road and its dot population across tile and surface publications. */
export function createRetention({ state, parts }) { export function createRetention({ state, parts }) {
const records = new Map(); const records = new Map();
const heightStep = { x: 0, y: 0, z: 0 }; const heightStep = new Cesium.Cartesian3();
const screenFrom = { x: 0, y: 0 }; const screenFrom = new Cesium.Cartesian2();
const screenTo = { x: 0, y: 0 }; const screenTo = new Cesium.Cartesian2();
const eye = { x: 0, y: 0, z: 0 }; const eye = new Cesium.Cartesian3();
function projectSafely(scene, point, screen) { function projectSafely(scene, point, screen) {
if (scene.project) return Boolean(scene.project(point, screen)); if (scene.project) return Boolean(scene.project(point, screen));
if (scene.camera.viewMatrix) { if (scene.camera.viewMatrix) {
+2 -1
View File
@@ -1,4 +1,5 @@
import { normalizeRoadMode } from './roadModes.js'; import { normalizeRoadMode } from './roadModes.js';
import * as Cesium from 'cesium';
import { FLOW_BUCKET_COLORS } from './policy.js'; import { FLOW_BUCKET_COLORS } from './policy.js';
export function createState({ services }) { export function createState({ services }) {
@@ -293,7 +294,7 @@ export function createState({ services }) {
/** Reusable scratch Cartesian3 to avoid per-frame allocation / GC pressure */ /** Reusable scratch Cartesian3 to avoid per-frame allocation / GC pressure */
layerState._scratchLerp = { x: 0, y: 0, z: 0 }; layerState._scratchLerp = new Cesium.Cartesian3();
// ─── Animation ───────────────────────────────────────────── // ─── Animation ─────────────────────────────────────────────
+3 -3
View File
@@ -1,4 +1,4 @@
// Ion world imagery asset IDs; the catalog is available before the SDK loads. import * as Cesium from 'cesium';
export const MAP_STACKS = [ export const MAP_STACKS = [
{ {
id: 'photoreal', id: 'photoreal',
@@ -12,7 +12,7 @@ export const MAP_STACKS = [
label: 'Bing Aerial', // i18n-ignore: imagery product name label: 'Bing Aerial', // i18n-ignore: imagery product name
shortLabel: 'Aerial', shortLabel: 'Aerial',
kind: 'ion', kind: 'ion',
style: 2, style: Cesium.IonWorldImageryStyle.AERIAL,
requiresIon: true, requiresIon: true,
}, },
{ {
@@ -20,7 +20,7 @@ export const MAP_STACKS = [
label: 'Bing Labels', // i18n-ignore: imagery product name label: 'Bing Labels', // i18n-ignore: imagery product name
shortLabel: 'Labels', shortLabel: 'Labels',
kind: 'ion', kind: 'ion',
style: 3, style: Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS,
requiresIon: true, requiresIon: true,
}, },
{ {