From 125fbc5a3bc14de87b81b5f95ae1ea26e59cf946 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Tue, 3 Nov 2026 23:21:00 +0400 Subject: [PATCH] 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. --- build/vite.js | 4 +- scripts/package-boundaries.json | 2 - src/annotations/worldAnnotationRenderer.js | 4 +- src/app/loadCesium.js | 52 -------------- src/app/loadCesium.test.mjs | 84 ---------------------- src/app/scene.js | 3 - src/cameraHandoff.test.mjs | 4 +- src/cyberSonarGpu.js | 17 +++-- src/data/bhoteKoshiEvent.js | 15 ++-- src/data/bhoteKoshiLocator.js | 24 +++---- src/data/geoImportPins.js | 18 ++--- src/data/modelVisualAnchor.js | 15 +++- src/data/render/cesiumProjection.js | 9 ++- src/globe/cesium/camera.js | 6 +- src/globe/cesium/frames.js | 3 +- src/globe/cesium/input.js | 26 +++---- src/globe/cesium/movers.js | 15 ++-- src/layers/directions/index.js | 17 +++-- src/layers/firms/render/cesium.js | 3 +- src/layers/gnss/render/cesium.js | 19 ++--- src/layers/streetLevel/view.js | 14 ++-- src/layers/submarineCables/policy.js | 8 +-- src/layers/traffic/footprint.js | 8 +-- src/layers/traffic/policy.js | 13 ++-- src/layers/traffic/retention.js | 8 +-- src/layers/traffic/state.js | 3 +- src/maps/catalog.js | 6 +- 27 files changed, 125 insertions(+), 275 deletions(-) delete mode 100644 src/app/loadCesium.js delete mode 100644 src/app/loadCesium.test.mjs diff --git a/build/vite.js b/build/vite.js index ac00a403a..351139fd3 100644 --- a/build/vite.js +++ b/build/vite.js @@ -186,9 +186,7 @@ export function createBrowserViteConfig({ apiResponseHeadersPlugin(), // Next, so no provider route answers a Host the server does not allow. hostCheckPlugin(), - // 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 }, + cesium(), // Application views are Vue single-file components. vue(VUE_PLUGIN_OPTIONS), ...plugins, diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 16a94865d..311e070dc 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -1984,7 +1984,6 @@ "traffic-layer": { "exports": ["./layers/traffic"], "modules": [ - "src/layers/shared/color.js", "src/data/cameraSensitivity.js", "src/data/flowMatch.js", "src/data/tomtomTiles.js", @@ -2658,7 +2657,6 @@ "src/app/layers/traffic.js", "src/app/operations.js", "src/app/scene.js", - "src/app/loadCesium.js", "src/sources/sourceSlot.js", "src/app/sources.js", "src/app/startupChrome.js", diff --git a/src/annotations/worldAnnotationRenderer.js b/src/annotations/worldAnnotationRenderer.js index e40ccad0d..189d46830 100644 --- a/src/annotations/worldAnnotationRenderer.js +++ b/src/annotations/worldAnnotationRenderer.js @@ -46,10 +46,10 @@ const PALETTE = { * * 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) { - const CLASSIFY = Cesium.ClassificationType.BOTH; - const CLAMP = Cesium.HeightReference.CLAMP_TO_GROUND; const dataSource = new Cesium.CustomDataSource('gev-annotations'); viewer.dataSources.add(dataSource); const osmAnnotations = new Set(); diff --git a/src/app/loadCesium.js b/src/app/loadCesium.js deleted file mode 100644 index 0701adba5..000000000 --- a/src/app/loadCesium.js +++ /dev/null @@ -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(); -} diff --git a/src/app/loadCesium.test.mjs b/src/app/loadCesium.test.mjs deleted file mode 100644 index d4e52da13..000000000 --- a/src/app/loadCesium.test.mjs +++ /dev/null @@ -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); -}); diff --git a/src/app/scene.js b/src/app/scene.js index 1d6664720..5609d7790 100644 --- a/src/app/scene.js +++ b/src/app/scene.js @@ -17,7 +17,6 @@ import { import { describeError } from './errors.js'; import { createSceneEngine } from './globeEngine.js'; import { onLocaleChange, t } from '../i18n/index.js'; -import { loadCesium } from './loadCesium.js'; /** * Construct the application globe using the caller's local configuration. @@ -140,8 +139,6 @@ async function createRenderer({ } if (engineKind !== 'cesium') throw new TypeError(`Unknown globe engine: ${engineKind}`); - await loadCesium(); - signal.throwIfAborted(); loaderStatus.textContent = t('Configuring viewer...'); // Provider attribution stays visible, including clean-view and recording. const creditContainer = document.createElement('div'); diff --git a/src/cameraHandoff.test.mjs b/src/cameraHandoff.test.mjs index b73036295..79497fd1a 100644 --- a/src/cameraHandoff.test.mjs +++ b/src/cameraHandoff.test.mjs @@ -1,6 +1,4 @@ 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 { readLayerSource } from './testSupport/readLayerSource.mjs'; 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(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( firms, /_clickHandler\.setInputAction\(\(click\) => \{([\s\S]*?)\n \}, clickType\);/, diff --git a/src/cyberSonarGpu.js b/src/cyberSonarGpu.js index ff4ffb94b..bb4aff4d6 100644 --- a/src/cyberSonarGpu.js +++ b/src/cyberSonarGpu.js @@ -9,7 +9,14 @@ export const CYBER_SONAR_CESIUM_VERSION = '1.138.0'; // Follow every public DrawCommand setter, including pick/shadow/culling flags. // Copying native.dirty would keep derived pipelines dirty forever: the scene // 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( native, @@ -18,14 +25,6 @@ export function syncCyberSonarCommand( uniforms, labelBackground, ) { - commandProperties ??= Object.entries( - Object.getOwnPropertyDescriptors(Cesium.DrawCommand.prototype), - ) - .filter( - ([key, descriptor]) => - descriptor.set && key !== 'shaderProgram' && key !== 'uniformMap', - ) - .map(([key]) => key); if (!record.command) { record.command = new Cesium.DrawCommand(); record.uniformMap = { diff --git a/src/data/bhoteKoshiEvent.js b/src/data/bhoteKoshiEvent.js index 0595f3fb4..25d29d99b 100644 --- a/src/data/bhoteKoshiEvent.js +++ b/src/data/bhoteKoshiEvent.js @@ -47,9 +47,8 @@ const SCENE_PATH_RENDER_HOLD = 'bhote-koshi-scene-path-travel'; const SCENE_MEDIA_MIN_PLAYBACK_SECONDS = 6; const SCENE_MEDIA_EXIT_SECONDS = 0.65; const BHOTE_EVENT_ACCENT = WORLD_OVERLAY_STYLE.accent; -const FLOOD_COLOR = () => - Cesium.Color.fromCssColorString('#d8954e').withAlpha(0.68); -const SURGE_COLOR = () => Cesium.Color.fromCssColorString('#e8b47c'); +const FLOOD_COLOR = Cesium.Color.fromCssColorString('#d8954e').withAlpha(0.68); +const SURGE_COLOR = Cesium.Color.fromCssColorString('#e8b47c'); const CINEMATIC_INPUT_EVENTS = Object.freeze(['pointerdown', 'wheel']); const CINEMATIC_VIEW_SPECS = Object.freeze([ { @@ -1078,8 +1077,8 @@ export function createBhoteKoshiEventLayer({ let _cinematicKeyframes = []; let _cinematicActive = false; let _cinematicReplayArmed = false; - let _cinematicPose = { target: { x: 0, y: 0, z: 0 } }; - let _cinematicOffset = { heading: 0, pitch: 0, range: 0 }; + let _cinematicPose = { target: new Cesium.Cartesian3() }; + let _cinematicOffset = new Cesium.HeadingPitchRange(); let _lastCinematicProgress = null; let _cameraInputTarget = null; let _evidenceClickTarget = null; @@ -2571,11 +2570,11 @@ export function createBhoteKoshiEventLayer({ const tileSafe = usesPhotorealFloodSurface(); _floodSurfaceMode = tileSafe ? 'tiles' : 'terrain'; const haloMaterial = new Cesium.PolylineDashMaterialProperty({ - color: FLOOD_COLOR().withAlpha(0.38), + color: FLOOD_COLOR.withAlpha(0.38), dashLength: 18, }); const coreMaterial = new Cesium.PolylineGlowMaterialProperty({ - color: FLOOD_COLOR().withAlpha(0.96), + color: FLOOD_COLOR.withAlpha(0.96), glowPower: 0.24, taperPower: 0.7, }); @@ -2620,7 +2619,7 @@ export function createBhoteKoshiEventLayer({ position: Cesium.Cartesian3.ZERO, point: { pixelSize: 11, - color: SURGE_COLOR(), + color: SURGE_COLOR, outlineColor: Cesium.Color.BLACK.withAlpha(0.85), outlineWidth: 3, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, diff --git a/src/data/bhoteKoshiLocator.js b/src/data/bhoteKoshiLocator.js index 3faa7eeb7..dece2920a 100644 --- a/src/data/bhoteKoshiLocator.js +++ b/src/data/bhoteKoshiLocator.js @@ -144,10 +144,10 @@ const NEARBY_CITIES = Object.freeze([ placement: 'right-lower', }), ]); -const BORDER_COLOR = () => Cesium.Color.fromCssColorString('#20e7f2'); +const BORDER_COLOR = Cesium.Color.fromCssColorString('#20e7f2'); const BORDER_ACCENT = '#20e7f2'; 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({ setEntries: setOverlayEntries, @@ -521,7 +521,7 @@ export function createBhoteKoshiLocatorLayer({ sequenceStartedAt, visibleAfterMs = 0, ) { - addCallout(dataSource, INCIDENT, INCIDENT_COLOR(), { + addCallout(dataSource, INCIDENT, INCIDENT_COLOR, { sequenceStartedAt, visibleAfterMs, }); @@ -555,12 +555,12 @@ export function createBhoteKoshiLocatorLayer({ false, ), 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()); return color; }, false), 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; return color; }, false), @@ -705,7 +705,7 @@ export function createBhoteKoshiLocatorLayer({ labelPlacement = 'right', { showAnchor = true } = {}, ) { - addCallout(dataSource, NEPAL, BORDER_COLOR(), { + addCallout(dataSource, NEPAL, BORDER_COLOR, { labelPlacement, sequenceStartedAt, showAnchor, @@ -820,12 +820,12 @@ export function createBhoteKoshiLocatorLayer({ false, ), 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()); return color; }, false), 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()); return color; }, false), @@ -890,7 +890,7 @@ export function createBhoteKoshiLocatorLayer({ positions: animatedPositions, width: 3.5, clampToGround: true, - material: BORDER_COLOR().withAlpha(0.96), + material: BORDER_COLOR.withAlpha(0.96), classificationType: Cesium.ClassificationType.BOTH, }, }); @@ -915,7 +915,7 @@ export function createBhoteKoshiLocatorLayer({ show: new Cesium.CallbackProperty(() => now() >= drawStartedAt, false), polygon: { hierarchy, - material: BORDER_COLOR().withAlpha(0.055), + material: BORDER_COLOR.withAlpha(0.055), classificationType: Cesium.ClassificationType.BOTH, }, }); @@ -925,7 +925,7 @@ export function createBhoteKoshiLocatorLayer({ positions: animatedPositions, width: 5, clampToGround: true, - material: BORDER_COLOR().withAlpha(0.12), + material: BORDER_COLOR.withAlpha(0.12), classificationType: Cesium.ClassificationType.BOTH, }, }); @@ -935,7 +935,7 @@ export function createBhoteKoshiLocatorLayer({ positions: animatedPositions, width: 2.5, clampToGround: true, - material: BORDER_COLOR().withAlpha(0.96), + material: BORDER_COLOR.withAlpha(0.96), classificationType: Cesium.ClassificationType.BOTH, }, }); diff --git a/src/data/geoImportPins.js b/src/data/geoImportPins.js index ac0a24a03..1828b5df7 100644 --- a/src/data/geoImportPins.js +++ b/src/data/geoImportPins.js @@ -21,17 +21,17 @@ const STYLE = [ 'imageSubRegion', ]; +class ImportedBillboards extends Cesium.BillboardCollection { + update(frameState) { + // Height queries should find the map, never another imported marker. + // Ordinary click picks and rendered screenshots still include the pins. + if (frameState.passes.pick && frameState.passes.offscreen) return; + super.update(frameState); + } +} + /** Own static imported pins without a per-frame Entity visualizer walk. */ export function createImportedPins(dataSource, viewer) { - class ImportedBillboards extends Cesium.BillboardCollection { - update(frameState) { - // Height queries should find the map, never another imported marker. - // Ordinary click picks and rendered screenshots still include the pins. - if (frameState.passes.pick && frameState.passes.offscreen) return; - super.update(frameState); - } - } - const all = dataSource.entities.values.slice(); const now = Cesium.JulianDate.now(); const pins = all.filter((entity) => entity.billboard && entity.position); diff --git a/src/data/modelVisualAnchor.js b/src/data/modelVisualAnchor.js index 3eedb2b22..2dd2302d7 100644 --- a/src/data/modelVisualAnchor.js +++ b/src/data/modelVisualAnchor.js @@ -1,3 +1,4 @@ +import { modelAxis } from './render/cesiumConstants.js'; import * as geo from '../geo/index.js'; /** * 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]), }); -// glTF Y-up/Z-forward to the model frame: Cesium's Y_UP_TO_Z_UP × -// Z_UP_TO_X_UP, shared with Navara before the Cesium SDK is loaded. -const AXIS_CORRECTION = [0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]; +/** @constant Cesium's OWN axis correction for a glTF loaded with the + * `Model.fromGltfAsync` defaults (upAxis Y, forwardAxis Z), assembled from its + * 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 _chainScratch = geo.matrix4.create(); diff --git a/src/data/render/cesiumProjection.js b/src/data/render/cesiumProjection.js index 1f53c148e..3075904c2 100644 --- a/src/data/render/cesiumProjection.js +++ b/src/data/render/cesiumProjection.js @@ -6,12 +6,11 @@ export function worldToWindowCoordinates(scene, position, result) { result, ); } -let occluder; +const occluder = new Cesium.EllipsoidalOccluder( + Cesium.Ellipsoid.WGS84, + new Cesium.Cartesian3(), +); export function horizonOccluder(camera) { - occluder ??= new Cesium.EllipsoidalOccluder( - Cesium.Ellipsoid.WGS84, - new Cesium.Cartesian3(), - ); occluder.cameraPosition = camera.positionWC; return occluder; } diff --git a/src/globe/cesium/camera.js b/src/globe/cesium/camera.js index bbc19f40f..cf247c3e6 100644 --- a/src/globe/cesium/camera.js +++ b/src/globe/cesium/camera.js @@ -9,11 +9,11 @@ import { toCesium2, toCesium3, toGeo3 } from './convert.js'; import { createCesiumFollow } from './follow.js'; const { math } = geo; -const easings = () => ({ +const EASINGS = { cubicInOut: Cesium.EasingFunction.CUBIC_IN_OUT, sinusoidalInOut: Cesium.EasingFunction.SINUSOIDAL_IN_OUT, linear: Cesium.EasingFunction.LINEAR_NONE, -}); +}; // Cesium reports heading and roll in [0, 2π]; the contract wants [0, 2π). function wrapAngle(angle) { @@ -43,7 +43,7 @@ const cesiumDestination = (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. const cesiumOffset = (offset) => diff --git a/src/globe/cesium/frames.js b/src/globe/cesium/frames.js index 35692dd09..2d2feee60 100644 --- a/src/globe/cesium/frames.js +++ b/src/globe/cesium/frames.js @@ -4,7 +4,7 @@ import * as Cesium from 'cesium'; import { bridgeEvent } from './events.js'; -let scratchGregorian; +const scratchGregorian = new Cesium.GregorianDate(); // JulianDate.toDate(time).getTime() without the Date: frame events must not // 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 }} */ export function createCesiumFrames(viewer) { - scratchGregorian ??= new Cesium.GregorianDate(); const { scene } = viewer; const bridges = ['preUpdate', 'preRender', 'postRender'].map((name) => [ name, diff --git a/src/globe/cesium/input.js b/src/globe/cesium/input.js index 57a8b5a84..7783e82f1 100644 --- a/src/globe/cesium/input.js +++ b/src/globe/cesium/input.js @@ -6,6 +6,19 @@ import { normalizeInputOptions } from '../contract.js'; import { createPickDelivery } from '../pickDelivery.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. const toEvent = (event) => { const result = {}; @@ -31,19 +44,6 @@ export function createCesiumInput( 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; let handler = null; let destroyed = false; diff --git a/src/globe/cesium/movers.js b/src/globe/cesium/movers.js index 8d2bfc7bb..9597d8ce6 100644 --- a/src/globe/cesium/movers.js +++ b/src/globe/cesium/movers.js @@ -8,12 +8,12 @@ import * as Cesium from 'cesium'; import { createMoverRegistry } from '../movers.js'; -let scratchPosition; -let scratchColor; -let scratchQuaternion; -let scratchRotation; +const scratchPosition = new Cesium.Cartesian3(); +const scratchColor = new Cesium.Color(); +const scratchQuaternion = new Cesium.Quaternion(); +const scratchRotation = new Cesium.Matrix3(); // 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). */ async function cropAtlasCells(atlas, keys) { @@ -287,11 +287,6 @@ export function createCesiumMovers( viewer, { 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 { scene } = viewer; let root = null; diff --git a/src/layers/directions/index.js b/src/layers/directions/index.js index 20bc20540..512d795c6 100644 --- a/src/layers/directions/index.js +++ b/src/layers/directions/index.js @@ -68,11 +68,10 @@ export function presentRoutingFailure(error) { /** Route colour — the annotation palette's cyan, so voice routes and Directions match. */ export const DIRECTIONS_ROUTE_COLOR = '#39d0ff'; -const MARKER_A_COLOR = () => Cesium.Color.fromCssColorString('#5dff9f'); -const MARKER_B_COLOR = () => Cesium.Color.fromCssColorString('#ff6b6b'); -const STEP_COLOR = () => Cesium.Color.WHITE.withAlpha(0.95); -const STEP_OUTLINE = () => - Cesium.Color.fromCssColorString(DIRECTIONS_ROUTE_COLOR); +const MARKER_A_COLOR = Cesium.Color.fromCssColorString('#5dff9f'); +const MARKER_B_COLOR = Cesium.Color.fromCssColorString('#ff6b6b'); +const STEP_COLOR = Cesium.Color.WHITE.withAlpha(0.95); +const STEP_OUTLINE = Cesium.Color.fromCssColorString(DIRECTIONS_ROUTE_COLOR); const STEP_PIXEL_SIZE = 8; const STEP_SELECTED_PIXEL_SIZE = 13; /** Route request timeout (ms) — the proxy itself gives OSRM 12 s. */ @@ -1027,10 +1026,10 @@ export function createDirectionsLayer({ if (!_viewer && !_native) return; if (letter === 'a') { removeEntity(_markerA); - _markerA = markerEntity('A', point, MARKER_A_COLOR()); + _markerA = markerEntity('A', point, MARKER_A_COLOR); } else { 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({ id: `directions:step:${index}`, position: Cesium.Cartesian3.fromDegrees(step.lon, step.lat, 0), - color: STEP_COLOR(), + color: STEP_COLOR, pixelSize: STEP_PIXEL_SIZE, - outlineColor: STEP_OUTLINE(), + outlineColor: STEP_OUTLINE, outlineWidth: 2, show: false, // The shared ground floor is a coarse cell, and on the photoreal stack diff --git a/src/layers/firms/render/cesium.js b/src/layers/firms/render/cesium.js index c40caad6a..0ccb20449 100644 --- a/src/layers/firms/render/cesium.js +++ b/src/layers/firms/render/cesium.js @@ -4,8 +4,7 @@ import { horizonOccluder, } from '../../../data/render/cesiumProjection.js'; -// Cesium ScreenSpaceEventType.LEFT_CLICK; available before the SDK loads. -export const FIRMS_LEFT_CLICK = 2; +export const FIRMS_LEFT_CLICK = Cesium.ScreenSpaceEventType.LEFT_CLICK; export const projectFirmsWindow = worldToWindowCoordinates; export const firmsHorizonOccluder = horizonOccluder; diff --git a/src/layers/gnss/render/cesium.js b/src/layers/gnss/render/cesium.js index 8e1e3c554..dccea9b27 100644 --- a/src/layers/gnss/render/cesium.js +++ b/src/layers/gnss/render/cesium.js @@ -1,6 +1,17 @@ import * as Cesium from 'cesium'; 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. */ export function defaultGnssViewAnchor(viewer) { const canvas = viewer?.scene?.canvas; @@ -33,14 +44,6 @@ export function createGnssCesiumRenderer({ viewer, 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(); primitives.id = 'gnss-integrity'; primitives.show = false; diff --git a/src/layers/streetLevel/view.js b/src/layers/streetLevel/view.js index 66f1408b0..702d33a18 100644 --- a/src/layers/streetLevel/view.js +++ b/src/layers/streetLevel/view.js @@ -227,7 +227,11 @@ export function viewFocus( * (NYC −22 m), and an occluder at 0 m would hide the cones around it. */ 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 @@ -236,18 +240,12 @@ let HORIZON_CULL_ELLIPSOID; * @param {{getViewer: () => object|null, items: () => Iterable<{position: object, show: boolean}>, onChange?: () => void}} options */ export function createHorizonCull({ getViewer, items, onChange }) { - let occluder; + const occluder = new Cesium.EllipsoidalOccluder(HORIZON_CULL_ELLIPSOID); let from = null; let stopListening = null; /** Show what is in front of the horizon, hide the rest; the count seen. */ 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; let seen = 0; let changed = false; diff --git a/src/layers/submarineCables/policy.js b/src/layers/submarineCables/policy.js index 9fb0c4341..89c18ca8c 100644 --- a/src/layers/submarineCables/policy.js +++ b/src/layers/submarineCables/policy.js @@ -86,15 +86,11 @@ export const CABLE_GLOBE_STACK_IDS = Object.freeze( export const MARKER_COLLECTION_EXPECTATIONS = Object.freeze([ Object.freeze({ key: '_billboardCollection', - get type() { - return Cesium.BillboardCollection; - }, + type: Cesium.BillboardCollection, }), Object.freeze({ key: '_pointCollection', - get type() { - return Cesium.PointPrimitiveCollection; - }, + type: Cesium.PointPrimitiveCollection, }), ]); diff --git a/src/layers/traffic/footprint.js b/src/layers/traffic/footprint.js index 1beb629fc..bbab2a673 100644 --- a/src/layers/traffic/footprint.js +++ b/src/layers/traffic/footprint.js @@ -68,7 +68,7 @@ export function createReticleFootprint() { const pixels = Array.from({ length: 25 }, () => geo.cartesian2.create()); const hits = Array.from({ length: 25 }, () => geo.cartesian3.create()); const carto = geo.cartographic.create(); - let ray; + const ray = new Cesium.Ray(); return (viewer, altitude, engine) => { const camera = engine?.camera ?? viewer?.camera; if (!camera?.getPickRay && !camera?.pickRay) return null; @@ -91,9 +91,7 @@ export function createReticleFootprint() { p = pixels[i]; p.x = left + (side * col) / 4; p.y = top + (side * row) / 4; - const hit = engine - ? camera.pickEllipsoid(p) - : camera.pickEllipsoid(p, Cesium.Ellipsoid.WGS84, hits[i]); + const hit = camera.pickEllipsoid(p, Cesium.Ellipsoid.WGS84, hits[i]); let dx, dy; if (hit) { geo.cartographic.fromCartesian(hit, geo.ellipsoid.WGS84, carto); @@ -104,7 +102,7 @@ export function createReticleFootprint() { } else { const picked = engine ? camera.pickRay(p) - : camera.getPickRay(p, (ray ??= new Cesium.Ray())) || ray; + : camera.getPickRay(p, ray) || ray; if (!picked) continue; const d = picked.direction; dx = diff --git a/src/layers/traffic/policy.js b/src/layers/traffic/policy.js index fd99d3b85..c20b15e41 100644 --- a/src/layers/traffic/policy.js +++ b/src/layers/traffic/policy.js @@ -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, @@ -112,9 +112,9 @@ export const SIZE_BY_TYPE = { */ export const FLOW_BUCKET_COLORS = { - free: rgbaFromCss('#2ecc71').withAlpha(0.9), - slow: rgbaFromCss('#f0b23e').withAlpha(0.9), - jam: rgbaFromCss('#e05252').withAlpha(0.9), + free: Cesium.Color.fromCssColorString('#2ecc71').withAlpha(0.9), + slow: Cesium.Color.fromCssColorString('#f0b23e').withAlpha(0.9), + jam: Cesium.Color.fromCssColorString('#e05252').withAlpha(0.9), }; // ─── 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). */ -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). */ -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). */ diff --git a/src/layers/traffic/retention.js b/src/layers/traffic/retention.js index c6cb3a519..0e51708db 100644 --- a/src/layers/traffic/retention.js +++ b/src/layers/traffic/retention.js @@ -12,10 +12,10 @@ export function roadIdentity(road) { /** Retain one road and its dot population across tile and surface publications. */ export function createRetention({ state, parts }) { const records = new Map(); - const heightStep = { x: 0, y: 0, z: 0 }; - const screenFrom = { x: 0, y: 0 }; - const screenTo = { x: 0, y: 0 }; - const eye = { x: 0, y: 0, z: 0 }; + const heightStep = new Cesium.Cartesian3(); + const screenFrom = new Cesium.Cartesian2(); + const screenTo = new Cesium.Cartesian2(); + const eye = new Cesium.Cartesian3(); function projectSafely(scene, point, screen) { if (scene.project) return Boolean(scene.project(point, screen)); if (scene.camera.viewMatrix) { diff --git a/src/layers/traffic/state.js b/src/layers/traffic/state.js index 50b19c535..f5331a5f9 100644 --- a/src/layers/traffic/state.js +++ b/src/layers/traffic/state.js @@ -1,4 +1,5 @@ import { normalizeRoadMode } from './roadModes.js'; +import * as Cesium from 'cesium'; import { FLOW_BUCKET_COLORS } from './policy.js'; export function createState({ services }) { @@ -293,7 +294,7 @@ export function createState({ services }) { /** Reusable scratch Cartesian3 to avoid per-frame allocation / GC pressure */ - layerState._scratchLerp = { x: 0, y: 0, z: 0 }; + layerState._scratchLerp = new Cesium.Cartesian3(); // ─── Animation ───────────────────────────────────────────── diff --git a/src/maps/catalog.js b/src/maps/catalog.js index 85dbae559..8934e63b3 100644 --- a/src/maps/catalog.js +++ b/src/maps/catalog.js @@ -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 = [ { id: 'photoreal', @@ -12,7 +12,7 @@ export const MAP_STACKS = [ label: 'Bing Aerial', // i18n-ignore: imagery product name shortLabel: 'Aerial', kind: 'ion', - style: 2, + style: Cesium.IonWorldImageryStyle.AERIAL, requiresIon: true, }, { @@ -20,7 +20,7 @@ export const MAP_STACKS = [ label: 'Bing Labels', // i18n-ignore: imagery product name shortLabel: 'Labels', kind: 'ion', - style: 3, + style: Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS, requiresIon: true, }, {