Load the Cesium SDK and its widget styles only when a Cesium globe first starts

This commit is contained in:
German Khokhlov committed 2026-11-03 22:56:24 +04:00
1 parent 9f8381ff71
commit ae13a56f6d
27 files changed
+260 -115

No files matched your search

+3 -1
View File
@@ -186,7 +186,9 @@ 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(),
cesium(), // Keep the SDK asset serving/copying and external imports, but let the
// 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,6 +1984,7 @@
"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",
@@ -2657,6 +2658,7 @@
"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
@@ -0,0 +1,52 @@
// 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
@@ -0,0 +1,84 @@
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,6 +17,7 @@ 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.
@@ -139,6 +140,8 @@ 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');
+3 -1
View File
@@ -1,4 +1,6 @@
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';
@@ -416,7 +418,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.match(firms, /FIRMS_LEFT_CLICK = Cesium\.ScreenSpaceEventType\.LEFT_CLICK/); assert.equal(FIRMS_LEFT_CLICK, 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\);/,
+9 -8
View File
@@ -9,14 +9,7 @@ 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.
const commandProperties = Object.entries( let commandProperties;
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,
@@ -25,6 +18,14 @@ 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 -7
View File
@@ -47,8 +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 = Cesium.Color.fromCssColorString('#d8954e').withAlpha(0.68); const FLOOD_COLOR = () => 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([
{ {
@@ -1077,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: new Cesium.Cartesian3() }; let _cinematicPose = { target: { x: 0, y: 0, z: 0 } };
let _cinematicOffset = new Cesium.HeadingPitchRange(); let _cinematicOffset = { heading: 0, pitch: 0, range: 0 };
let _lastCinematicProgress = null; let _lastCinematicProgress = null;
let _cameraInputTarget = null; let _cameraInputTarget = null;
let _evidenceClickTarget = null; let _evidenceClickTarget = null;
@@ -2570,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,
}); });
@@ -2619,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,
@@ -518,7 +518,7 @@ export function createBhoteKoshiLocatorLayer({
sequenceStartedAt, sequenceStartedAt,
visibleAfterMs = 0, visibleAfterMs = 0,
) { ) {
addCallout(dataSource, INCIDENT, INCIDENT_COLOR, { addCallout(dataSource, INCIDENT, INCIDENT_COLOR(), {
sequenceStartedAt, sequenceStartedAt,
visibleAfterMs, visibleAfterMs,
}); });
@@ -552,12 +552,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),
@@ -695,7 +695,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,
@@ -810,12 +810,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),
@@ -880,7 +880,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,
}, },
}); });
@@ -905,7 +905,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,
}, },
}); });
@@ -915,7 +915,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,
}, },
}); });
@@ -925,7 +925,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,
}, },
}); });
+2 -2
View File
@@ -21,6 +21,8 @@ const STYLE = [
'imageSubRegion', 'imageSubRegion',
]; ];
/** Own static imported pins without a per-frame Entity visualizer walk. */
export function createImportedPins(dataSource, viewer) {
class ImportedBillboards extends Cesium.BillboardCollection { 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.
@@ -30,8 +32,6 @@ class ImportedBillboards extends Cesium.BillboardCollection {
} }
} }
/** 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);
+3 -12
View File
@@ -1,4 +1,3 @@
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.
@@ -71,17 +70,9 @@ 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]),
}); });
/** @constant Cesium's OWN axis correction for a glTF loaded with the // glTF Y-up/Z-forward to the model frame: Cesium's Y_UP_TO_Z_UP ×
* `Model.fromGltfAsync` defaults (upAxis Y, forwardAxis Z), assembled from its // Z_UP_TO_X_UP, shared with Navara before the Cesium SDK is loaded.
* exported constants rather than restated as numbers here. `ModelSceneGraph` const AXIS_CORRECTION = [0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1];
* 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 -2
View File
@@ -6,11 +6,12 @@ export function worldToWindowCoordinates(scene, position, result) {
result, result,
); );
} }
const occluder = new Cesium.EllipsoidalOccluder( let occluder;
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) =>
+2 -1
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';
const scratchGregorian = new Cesium.GregorianDate(); let scratchGregorian;
// 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,6 +27,7 @@ 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,19 +6,6 @@ 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 = {};
@@ -44,6 +31,19 @@ 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;
+10 -5
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';
const scratchPosition = new Cesium.Cartesian3(); let scratchPosition;
const scratchColor = new Cesium.Color(); let scratchColor;
const scratchQuaternion = new Cesium.Quaternion(); let scratchQuaternion;
const scratchRotation = new Cesium.Matrix3(); let scratchRotation;
// Model.modelMatrix and Model.color copy what they are given. // Model.modelMatrix and Model.color copy what they are given.
const scratchModelMatrix = new Cesium.Matrix4(); let scratchModelMatrix;
/** 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,6 +287,11 @@ 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 -8
View File
@@ -68,10 +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 = Cesium.Color.fromCssColorString(DIRECTIONS_ROUTE_COLOR); const STEP_OUTLINE = () => 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. */
@@ -1020,10 +1020,10 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
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());
} }
} }
@@ -1127,9 +1127,9 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
_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
+2 -1
View File
@@ -4,7 +4,8 @@ import {
horizonOccluder, horizonOccluder,
} from '../../../data/render/cesiumProjection.js'; } from '../../../data/render/cesiumProjection.js';
export const FIRMS_LEFT_CLICK = Cesium.ScreenSpaceEventType.LEFT_CLICK; // Cesium ScreenSpaceEventType.LEFT_CLICK; available before the SDK loads.
export const FIRMS_LEFT_CLICK = 2;
export const projectFirmsWindow = worldToWindowCoordinates; export const projectFirmsWindow = worldToWindowCoordinates;
export const firmsHorizonOccluder = horizonOccluder; export const firmsHorizonOccluder = horizonOccluder;
+8 -11
View File
@@ -1,17 +1,6 @@
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;
@@ -44,6 +33,14 @@ 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;
+8 -6
View File
@@ -210,11 +210,7 @@ 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;
const HORIZON_CULL_ELLIPSOID = new Cesium.Ellipsoid( let HORIZON_CULL_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
@@ -223,12 +219,18 @@ const HORIZON_CULL_ELLIPSOID = new Cesium.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 }) {
const occluder = new Cesium.EllipsoidalOccluder(HORIZON_CULL_ELLIPSOID); let occluder;
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 -2
View File
@@ -86,11 +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',
type: Cesium.BillboardCollection, get type() { return Cesium.BillboardCollection; },
}), }),
Object.freeze({ Object.freeze({
key: '_pointCollection', key: '_pointCollection',
type: Cesium.PointPrimitiveCollection, get type() { return Cesium.PointPrimitiveCollection; },
}), }),
]); ]);
+5 -3
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();
const ray = new Cesium.Ray(); let 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,7 +91,9 @@ 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 = camera.pickEllipsoid(p, Cesium.Ellipsoid.WGS84, hits[i]); const hit = engine
? 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);
@@ -100,7 +102,7 @@ export function createReticleFootprint() {
dlon -= 360 * Math.round(dlon / 360); dlon -= 360 * Math.round(dlon / 360);
dx = dlon * 111.195 * Math.cos(nadir.latitude); dx = dlon * 111.195 * Math.cos(nadir.latitude);
} else { } else {
const picked = engine ? camera.pickRay(p) : camera.getPickRay(p, ray) || ray; const picked = engine ? camera.pickRay(p) : camera.getPickRay(p, (ray ??= new Cesium.Ray())) || ray;
if (!picked) continue; if (!picked) continue;
const d = picked.direction; const d = picked.direction;
dx = dx =
+6 -6
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium'; import { rgbaFromCss } from '../shared/color.js';
/** /**
* @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: Cesium.Color.fromCssColorString('#2ecc71').withAlpha(0.9), free: rgbaFromCss('#2ecc71').withAlpha(0.9),
slow: Cesium.Color.fromCssColorString('#f0b23e').withAlpha(0.9), slow: rgbaFromCss('#f0b23e').withAlpha(0.9),
jam: Cesium.Color.fromCssColorString('#e05252').withAlpha(0.9), jam: rgbaFromCss('#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,12 +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 = Cesium.Color.fromCssColorString('#e05252'); export const HEAT_JAM_COLOR = rgbaFromCss('#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 = export const HEAT_SLOW_COLOR =
Cesium.Color.fromCssColorString('#f0b23e').withAlpha(0.2); rgbaFromCss('#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 = new Cesium.Cartesian3(); const heightStep = { x: 0, y: 0, z: 0 };
const screenFrom = new Cesium.Cartesian2(); const screenFrom = { x: 0, y: 0 };
const screenTo = new Cesium.Cartesian2(); const screenTo = { x: 0, y: 0 };
const eye = new Cesium.Cartesian3(); const eye = { x: 0, y: 0, z: 0 };
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) {
+1 -2
View File
@@ -1,5 +1,4 @@
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 }) {
@@ -294,7 +293,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 = new Cesium.Cartesian3(); layerState._scratchLerp = { x: 0, y: 0, z: 0 };
// ─── Animation ───────────────────────────────────────────── // ─── Animation ─────────────────────────────────────────────
+3 -3
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium'; // Ion world imagery asset IDs; the catalog is available before the SDK loads.
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: Cesium.IonWorldImageryStyle.AERIAL, style: 2,
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: Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS, style: 3,
requiresIon: true, requiresIon: true,
}, },
{ {