refactor(overlays): hand the shell's overlay host the engine, not the viewer

The controls pass the scene engine to the shell, which initializes the
world overlay on it; initWorldOverlay takes only a globe engine (a viewer
or null is refused), so the shell no longer needs a viewer for its
overlay. engineFromViewer only returns the engine registered for a viewer
(the scene engine until teardown) and never builds one, so no engine can
outlive its viewer without a destroy path. Overlay and detection tests
and the allocation worker build a Cesium engine on their mock viewers and
destroy it with them; frame allocation is unchanged.
This commit is contained in:
German Khokhlov committed 2026-10-15 18:53:57 +04:00
1 parent 3e3aa0bd9f
commit 12c9651a67
10 files changed
+145 -105

No files matched your search

+3 -1
View File
@@ -5,7 +5,7 @@ import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
/** Construct the existing controls and camera presentation. */
export function createApplicationControls({
scene: { viewer, mapStackController, operations },
scene: { viewer, engine, mapStackController, operations },
loaderStatus,
Controls = StyleManager,
services,
@@ -24,6 +24,8 @@ export function createApplicationControls({
...catalogControlServices(catalog),
},
requestServices: operations.requests,
// The page's globe engine: the shell's overlay host runs on it.
engine,
mapStackController,
placeSearch,
});
+23
View File
@@ -0,0 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
// controls.js and the shell import browser-only UI modules, so these read the
// source: the scene engine is threaded to the shell, which hands it (not the
// viewer) to the world overlay. initWorldOverlay itself is tested on the fake
// engine without a viewer (src/overlays/worldOverlay.test.mjs).
const read = (relative) =>
readFileSync(new URL(relative, import.meta.url), 'utf8');
test('the controls pass the scene engine to the shell', () => {
const controls = read('./controls.js');
assert.match(controls, /scene: \{ viewer, engine, mapStackController, operations \}/);
assert.match(controls, /new Controls\(viewer, \{[\s\S]*?\n\s+engine,\n/);
});
test('the shell initializes the world overlay on its engine', () => {
const shell = read('../ui/applicationShell.js');
assert.match(shell, /\{\n\s+engine = null,\n\s+mapStackController = null,/);
assert.match(shell, /initWorldOverlay\(engine\);/);
assert.doesNotMatch(shell, /initWorldOverlay\(viewer\)/);
});
+4 -4
View File
@@ -80,9 +80,9 @@ test('the scene engine is the one engineFromViewer returns for its viewer until
assert.equal(engineFromViewer(viewer), engine);
for (const cleanup of cleanups.reverse()) cleanup();
assert.equal(engine.fake.destroyed, true);
assert.throws(
() => engineFromViewer(viewer),
/createCesiumEngine needs a viewer/,
'no longer registered: a fresh one would be built',
assert.equal(
engineFromViewer(viewer),
null,
'no longer registered, and none is built',
);
});
+16 -11
View File
@@ -3,6 +3,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import * as Cesium from 'cesium';
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
import {
countFadingRenderEntries,
destroyDetection,
@@ -247,8 +248,11 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
scene: { postRender, requestRender() {} },
};
// Shell modules take the globe engine; the Cesium engine runs on the mock.
const engine = createCesiumEngine(viewer);
return {
viewer,
engine,
postRender,
document,
ctx,
@@ -259,6 +263,7 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
cleanup() {
destroyDetection();
destroyWorldOverlay();
engine.destroy();
Date.now = originalDateNow;
if (originalPath2D === undefined) delete globalThis.Path2D;
else globalThis.Path2D = originalPath2D;
@@ -353,7 +358,7 @@ test('detection lifecycle re-hosts unchanged painters behind the sole host liste
const env = installEnvironment({ search: '?detectDebug=1' });
const modes = [];
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(env.viewer, [detectableLayer()], (mode) => modes.push(mode));
assert.equal(env.postRender.listeners.size, 1);
assert.equal(env.document.getElementById('detection-overlay'), null);
@@ -507,7 +512,7 @@ test('callouts stop painting the moment the last detectable object goes away', (
type: 'AIR',
}];
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(env.viewer, [{ id: 'flights', getDetectableObjects: () => objects }], () => {});
setMode('DENSE');
env.advance(250);
@@ -543,7 +548,7 @@ test('every style paints its OWN plate token through the production callout path
// path once per style and reads the fill style the shared canvas received.
const env = installEnvironment();
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(env.viewer, [mixedTierLayer()], () => {});
setMode('DENSE');
settleFrame(env);
@@ -581,7 +586,7 @@ test('space-tier callouts take the heavier plate while air contacts keep the lig
// tiers at once can show it.
const env = installEnvironment();
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(env.viewer, [mixedTierLayer()], () => {});
setDetectionStyle('surveillance');
setMode('DENSE');
@@ -609,7 +614,7 @@ test('space-tier callouts take the heavier plate while air contacts keep the lig
test('surveillance military tier reaches the dedicated render target as shipped red', () => {
const env = installEnvironment();
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(env.viewer, [militaryLayer()], () => {});
setDetectionStyle('surveillance');
setMode('DENSE');
@@ -631,7 +636,7 @@ test('pathological detection paint holds alternate frames without freezing share
// Opts into the telemetry banner: the held-frame assertions count banner paints.
const env = installEnvironment({ search: '?detectDebug=1' });
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(env.viewer, [detectableLayer()], () => {});
setOverlayEntries('valve-card', [{
id: 'ambient-card',
@@ -738,7 +743,7 @@ test('the mode banner is absent by default and present behind the flag', () => {
const painted = (search) => {
const env = installEnvironment({ search });
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(env.viewer, [detectableLayer()], () => {});
setMode('DENSE');
env.advance(250);
@@ -765,7 +770,7 @@ test('detection telemetry stays reachable programmatically with the banner hidde
// is the supported way to read them, and is what the QA harnesses use.
const env = installEnvironment();
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(env.viewer, [detectableLayer()], () => {});
setMode('DENSE');
env.advance(250);
@@ -793,7 +798,7 @@ test('civilian and military AIR brackets cover front, left, and right at Sparse
metric: 'FL120',
type: 'AIR',
}));
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(env.viewer, [{ id: layerId, getDetectableObjects: () => objects }], () => {});
setDetectionTuning({ densityPct: 25 });
setMode('SPARSE');
@@ -851,7 +856,7 @@ test('the backdrop feather reaches the canvas as a lighter plate against sky', (
// the shared canvas actually received for each plate.
const env = installEnvironment();
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(env.viewer, [backdropLayer()], () => {});
setDetectionStyle('normal');
setMode('DENSE');
@@ -909,7 +914,7 @@ test('a transit vehicle is a contact: box, route, mode and operator on the share
},
];
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initDetection(
env.viewer,
[{ id: 'transit', getDetectableObjects: () => objects }],
+8 -16
View File
@@ -1,12 +1,10 @@
// The bridge for shell callers that still hold only a Cesium viewer (layers
// until phase 1c, standalone entries, tests): the engine of that viewer. The
// app registers its scene engine (src/app/globeEngine.js); any other viewer
// gets one built on first use and cached. Building one changes nothing in
// the viewer, and it is never destroyed here: its subscriptions live only as
// long as their callers keep them.
// until phase 1c): the engine registered for that viewer. The app registers
// its scene engine (src/app/globeEngine.js) and unregisters it before
// destroying it. Nothing is ever built here, so no engine can outlive its
// viewer without a destroy path: an unregistered or torn-down viewer has none.
import { isGlobeEngine } from '../contract.js';
import { createCesiumEngine } from './createCesiumEngine.js';
const engines = new WeakMap();
@@ -24,17 +22,11 @@ export function registerViewerEngine(viewer, engine) {
/**
* @param {object | null | undefined} viewer A Cesium viewer, or an engine
* (returned as is).
* @param {Parameters<typeof createCesiumEngine>[1]} [options] Used only when
* an engine has to be built.
* @returns {import('../contract.js').GlobeEngine | null}
* @returns {import('../contract.js').GlobeEngine | null} The registered
* engine, or null.
*/
export function engineFromViewer(viewer, options) {
export function engineFromViewer(viewer) {
if (viewer == null) return null;
if (isGlobeEngine(viewer)) return viewer;
let engine = engines.get(viewer);
if (!engine) {
engine = createCesiumEngine(viewer, options);
engines.set(viewer, engine);
}
return engine;
return engines.get(viewer) ?? null;
}
+8 -11
View File
@@ -5,28 +5,25 @@ import { createFakeEngine } from '../testing/fakeEngine.js';
import { engineFromViewer, registerViewerEngine } from './engineFromViewer.js';
import { createFakeViewer } from './testing/fakeViewer.mjs';
test('a viewer without a registered engine gets one built once and cached', () => {
test('a viewer without a registered engine has none: nothing is built', () => {
const viewer = createFakeViewer();
const engine = engineFromViewer(viewer);
assert.equal(isGlobeEngine(engine), true);
assert.equal(engine.kind, 'cesium');
assert.equal(engineFromViewer(viewer), engine);
assert.deepEqual(viewer.calls, [], 'building it changes nothing');
assert.notEqual(engineFromViewer(createFakeViewer()), engine);
assert.equal(engineFromViewer(viewer), null);
assert.deepEqual(viewer.calls, []);
});
test('the engine registered for a viewer is the one returned', () => {
test('the engine registered for a viewer is returned until it is unregistered', () => {
const viewer = createFakeViewer();
const engine = createFakeEngine();
const unregister = registerViewerEngine(viewer, engine);
assert.equal(engineFromViewer(viewer), engine);
unregister();
const built = engineFromViewer(viewer);
assert.notEqual(built, engine);
assert.equal(engineFromViewer(viewer), null, 'a torn-down viewer has none');
const next = createFakeEngine();
registerViewerEngine(viewer, next);
unregister();
assert.equal(
engineFromViewer(viewer),
built,
next,
'a stale unregister keeps the new one',
);
});
+5 -9
View File
@@ -1,6 +1,6 @@
import * as Cesium from 'cesium';
import * as geo from '../geo/index.js';
import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
import { isGlobeEngine } from '../globe/contract.js';
import {
createCyberSonarSampler,
isCyberContactSonarActive as isCyberSonarActive,
@@ -2664,15 +2664,11 @@ function createDevFacade() {
/**
* Initialize the singleton host. Repeated calls for the same engine are no-op.
* @param {import('../globe/contract.js').GlobeEngine | object} target The
* globe engine, or a Cesium viewer (its engine: engineFromViewer).
* @param {import('../globe/contract.js').GlobeEngine} engine
*/
export function initWorldOverlay(target) {
const engine = engineFromViewer(target);
if (!engine)
throw new TypeError(
'initWorldOverlay requires a globe engine or a Cesium viewer',
);
export function initWorldOverlay(engine) {
if (!isGlobeEngine(engine))
throw new TypeError('initWorldOverlay requires a globe engine');
if (_engine === engine && _canvas && _removePostRender) return;
if (_engine) destroyWorldOverlay();
_destroyed = false;
+63 -49
View File
@@ -5,6 +5,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import * as Cesium from 'cesium';
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
import {
getKeyholeGeometry,
keyholeLabelAlphaFromGeometry,
@@ -379,8 +380,11 @@ function installMockEnvironment({
},
};
// Shell modules take the globe engine; the Cesium engine runs on the mock.
const engine = createCesiumEngine(viewer);
return {
viewer,
engine,
ctx,
detectionCtx,
paintTrace,
@@ -395,6 +399,7 @@ function installMockEnvironment({
advanceTime(ms) { currentTime += ms; },
cleanup() {
destroyWorldOverlay();
engine.destroy();
delete globalThis.document;
delete globalThis.window;
delete globalThis.ResizeObserver;
@@ -435,7 +440,7 @@ test('destroying before the first init leaves pre-init buffering intact', () =>
setOverlayEntries('pre-init', [selectedEntry('buffered')]);
assert.equal(getWorldOverlayDiagnostics().entryCount, 1);
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
env.postRender.raise();
assert.equal(getWorldOverlayDiagnostics().paintedCount, 1);
@@ -450,8 +455,8 @@ test('destroying before the first init leaves pre-init buffering intact', () =>
test('lifecycle is idempotent and teardown removes listeners, observers, and DOM', () => {
const env = installMockEnvironment();
assert.equal(env.document.getElementById('world-overlay-detection-surface'), null);
initWorldOverlay(env.viewer);
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
initWorldOverlay(env.engine);
assert.equal(env.postRender.listeners.size, 1);
assert.equal(env.moveEnd.listeners.size, 1);
assert.equal(env.window.listenerCount('gev:cockpit-mode-changed'), 1);
@@ -553,7 +558,7 @@ test('no ancestor isolates the detection surface, so `screen` reaches the scene'
// changed was containment. This walks the ancestor chain the host actually
// builds and evaluates each node against the shipped stylesheet.
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const css = readStylesheet(new URL('../../style.css', import.meta.url));
const surface = env.document.getElementById('world-overlay-detection-surface');
assert.ok(surface, 'the host owns a detection surface');
@@ -585,7 +590,7 @@ test('no ancestor isolates the detection surface, so `screen` reaches the scene'
test('canvas backing store tracks CSS size and live DPR', () => {
const env = installMockEnvironment({ width: 400, height: 300, dpr: 2 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
let canvas = env.document.getElementById('world-overlay-canvas');
let detectionSurface = env.document.getElementById('world-overlay-detection-surface');
assert.equal(canvas.width, 0);
@@ -619,7 +624,7 @@ test('shared fade tuning reaches a host-painted card on the next rendered frame'
});
try {
setKeyholeFadeTuning({ fadeRatio: 0.16, outsideOpacity: 0.05 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('fade-host', [selectedEntry('CARD', {
position: new Cesium.Cartesian3(0.85, 0, 0),
variant: 'card',
@@ -649,7 +654,7 @@ test('inlined host alpha binding matches combinedOverlayAlpha across channel ran
get() { return paintedAlphas.at(-1) ?? 1; },
set(value) { paintedAlphas.push(value); },
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const cases = [
{ source: 1, entry: 0.9, temporal: 0.8, distance: 0.25, altitude: 0.25 },
{ source: 0.8, entry: 0.75, temporal: 0.7, distance: 0.5, altitude: 0.5 },
@@ -695,7 +700,7 @@ test('inlined host alpha binding matches combinedOverlayAlpha across channel ran
test('a host dormant since init holds no canvas backing store', () => {
const env = installMockEnvironment({ width: 1600, height: 900, dpr: 2 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const canvas = env.document.getElementById('world-overlay-canvas');
const detectionSurface = env.document.getElementById('world-overlay-detection-surface');
assert.equal(canvas.width, 0);
@@ -733,7 +738,7 @@ test('a host dormant since init holds no canvas backing store', () => {
test('empty postRender stays inert through production mutation, resize, and clock noise', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
env.postRender.raise();
const settledCallCount = env.ctx.calls.length;
const settledLayoutReads = { ...env.layoutReads };
@@ -752,7 +757,7 @@ test('empty postRender stays inert through production mutation, resize, and cloc
test('HUD mutations defer occluder selector scans until overlay paint work exists', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const settledQueries = { ...env.selectorQueries };
const hudMutation = {
type: 'childList',
@@ -777,7 +782,7 @@ test('ticking chrome churn (text swaps, non-chrome nodes) never invalidates a li
{ id: 'traffic-sync-chip', rect: { left: 200, top: 0, width: 90, height: 24 } },
],
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('ticker-noise', [selectedEntry('live-entry')]);
env.postRender.raise();
@@ -819,7 +824,7 @@ test('genuine chrome changes still invalidate: add, remove, and own-attribute fl
{ id: 'pp-toggles', rect: { left: 300, top: 40, width: 60, height: 200 } },
],
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('chrome-changes', [selectedEntry('live-entry')]);
env.postRender.raise();
const observer = env.mutationObservers[0];
@@ -859,7 +864,7 @@ test('chrome observers scope: body is childList discovery; occluder attributes a
{ selector: '.hud-top-left', rect: { left: 0, top: 0, width: 140, height: 44 } },
],
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('observer-scope', [selectedEntry('live-entry')]);
env.postRender.raise();
@@ -909,7 +914,7 @@ test('projection records never retain keys for entries that are no longer live',
const env = installMockEnvironment();
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
for (let batch = 0; batch < 4; batch++) {
const entries = Array.from({ length: 320 }, (_, index) => (
selectedEntry(`batch-${batch}-${index}`, {
@@ -944,7 +949,7 @@ test('projection records never retain keys for entries that are no longer live',
test('the per-domain candidate index stays bounded while identities churn', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const COHORT = 40;
const BATCHES = 60;
const batchEntries = (batch) => Array.from({ length: COHORT }, (_, index) => (
@@ -1003,7 +1008,7 @@ test('entry normalization validates required fields and source lifecycle is stab
assert.throws(() => normalizeOverlayEntry('fires', { id: 'a', position: position(), variant: 'table' }), /variant/);
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('fires', [normalized]);
upsertOverlayEntry('fires', { id: 'b', position: position(), variant: 'card' });
assert.deepEqual(getWorldOverlayDiagnostics().entriesBySource, { fires: 2 });
@@ -1019,7 +1024,7 @@ test('accessible actions announce only accepted focus and expose selected state'
const env = installMockEnvironment();
let acceptedActivations = 0;
let staleActivations = 0;
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('accessible-actions', [selectedEntry('vessel:123', {
interactive: true,
accessibilityLabel: 'Focus vessel TEST, MMSI 123',
@@ -1092,7 +1097,7 @@ test('viewport and horizon culling obey explicit policy inputs', () => {
test('paint lanes are deterministic across all seven binding lanes', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
assert.deepEqual(WORLD_OVERLAY_PAINT_LANES, [
'detection',
'ambient-label',
@@ -1136,7 +1141,7 @@ test('custom detection lane receives the shared host frame and paints below ordi
dpr: 2,
occluderRect: { left: 10, top: 20, width: 80, height: 40 },
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
let capturedFrame = null;
let paintCount = 0;
const lane = registerWorldOverlayPaintLane('detection', (frame) => {
@@ -1200,7 +1205,7 @@ test('custom-lane painter state is bracketed by host save/restore', () => {
// host wraps every custom-lane painter in save/try/finally-restore. Pin the
// bracketing structurally (the mock ctx records call order, not state).
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const lane = registerWorldOverlayPaintLane('detection', (frame) => {
frame.ctx.beginPath();
frame.ctx.rect(0, 0, 1, 1);
@@ -1227,7 +1232,7 @@ test('custom-lane painter state is bracketed by host save/restore', () => {
test('custom lanes without the detection target paint on the shared canvas', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
let target = null;
registerWorldOverlayPaintLane('ambient-label', (frame) => {
target = { surface: frame.surface, ctx: frame.ctx };
@@ -1279,7 +1284,7 @@ test('UI exclusions stay per-rect: overlapping chrome never merges into a boundi
{ id: 'command-dock', rect: chrome.bottom },
],
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
let uiRects = null;
registerWorldOverlayPaintLane('detection', (frame) => {
uiRects = frame.uiRects.slice(0, frame.uiRectCount)
@@ -1336,7 +1341,7 @@ test('UI exclusions ignore chrome the user cannot see', () => {
{ id: 'pp-toggles', rect: { left: 0, top: 0, width: 0, height: 0 } },
],
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
let uiRectCount = -1;
registerWorldOverlayPaintLane('detection', (frame) => { uiRectCount = frame.uiRectCount; }, {
id: 'detection',
@@ -1363,7 +1368,7 @@ test('chrome that fills the viewport suppresses neither cards nor the detection
dpr: 1,
occluderRect: { left: 0, top: 0, width: 400, height: 300 },
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
let detectionPaints = 0;
registerWorldOverlayPaintLane('detection', (frame) => {
detectionPaints++;
@@ -1402,7 +1407,7 @@ test('a placement is only ever kept under chrome that composites ABOVE the host'
const paintedUnder = (occluders) => {
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1, occluders });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('ambient', [selectedEntry('CONTACT')]);
env.postRender.raise();
const { paintedCount } = getWorldOverlayDiagnostics();
@@ -1446,7 +1451,7 @@ test('cockpit keeps its cards and its detection lane, and hides only the tracked
{ selector: '.cockpit-roll-arc', inInventory: false, rect: { left: 88, top: 2, width: 224, height: 32 } },
],
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
let uiRectCount = -1;
let detectionPaints = 0;
registerWorldOverlayPaintLane('detection', (frame) => {
@@ -1492,7 +1497,7 @@ test('opt-in anchor separation thins a co-located cohort before the arbiter sees
const anchors = [500, 540, 580, 620, 660, 700];
const project = (separation) => {
const env = installMockEnvironment({ width: 400, height: 800, dpr: 1 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('cctv', anchors.map((y, index) => selectedEntry(`cam-${index}`, {
position: positionAtScreen(200, y, 400, 800),
protected: false,
@@ -1532,7 +1537,7 @@ test('the occluder inventory carries no cockpit line-art chrome', () => {
test('position getters snapshot once per frame and cockpit-gated sources disappear', () => {
const env = installMockEnvironment();
let getterCalls = 0;
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('tracked', [selectedEntry('flight', {
position: () => { getterCalls++; return position(); },
interactive: true,
@@ -1568,7 +1573,7 @@ test('bounded per-source cohorts feed the domain arbiter without universal selec
assert.equal(selectBoundedOverlayCohort([...normalized, protectedEntry], 3).length, 4);
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('bulk', normalized, { cohortLimit: 3, collisionCapacity: 2 });
env.postRender.raise();
const diagnostics = getWorldOverlayDiagnostics();
@@ -1580,7 +1585,7 @@ test('bounded per-source cohorts feed the domain arbiter without universal selec
test('FIRMS and vessels enlarge the ambient-card lane under an explicit aggregate budget', () => {
const env = installMockEnvironment({ width: 1600, height: 900, dpr: 1 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const makeEntries = (sourceOffset, count = 200) => Array.from({ length: count }, (_, index) => {
const slot = index % 160;
const column = slot % 16;
@@ -1637,7 +1642,7 @@ test('FIRMS and vessels enlarge the ambient-card lane under an explicit aggregat
test('paint rectangles publish from a pool and topmost interactive lookup wins', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('hits', [
selectedEntry('under', { interactive: true, zIndex: 1, collisionGroup: 'under' }),
selectedEntry('over', { interactive: true, zIndex: 2, collisionGroup: 'over' }),
@@ -1662,7 +1667,7 @@ test('paint rectangles publish from a pool and topmost interactive lookup wins',
test('CCTV stable frame slots gate ambient chrome and wire the exact host hit rectangle', () => {
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const slot = createFrameSlot();
const entry = createCctvThumbnailOverlayEntry({
id: 'cam-a',
@@ -1711,7 +1716,7 @@ test('CCTV stable frame slots gate ambient chrome and wire the exact host hit re
test('pinned CCTV chrome may paint before frame one while ordinary safe-top policy still yields', () => {
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const ambient = createCctvThumbnailOverlayEntry({
id: 'ambient-top',
position: new Cesium.Cartesian3(0, 0.9, 0),
@@ -1741,7 +1746,7 @@ test('safe-top yield culls an uncontested ambient entry at projection, with a be
// pinned neighbor to arbiter-collide with, so only the projection gate can
// null it; the below-band twin proves the paint path itself is live.
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const above = createCctvThumbnailOverlayEntry({
id: 'yield-above',
position: new Cesium.Cartesian3(-0.5, 0.9, 0),
@@ -1779,7 +1784,7 @@ test('CCTV host binding preserves the smoothstep scale and 0.45/0.35 fade curve'
get() { return paintedAlphas.at(-1) ?? 1; },
set(value) { paintedAlphas.push(value); },
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const entry = createCctvThumbnailOverlayEntry({
id: 'altitude-curve',
position: position(),
@@ -1830,7 +1835,7 @@ test('scene presentation samples once per frame and recovers after a callback fa
get() { return alphas.at(-1) ?? 1; },
set(value) { alphas.push(value); },
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const entry = createCctvThumbnailOverlayEntry({
id: 'scene-presentation', position: position(), title: 'SCENE',
frameSlot: { frame: { ready: true }, stamp: 1 }, active: true,
@@ -1904,7 +1909,7 @@ test('scene presentation samples once per frame and recovers after a callback fa
test('CCTV leader remains one CSS pixel at the 0.35 altitude scale point', () => {
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
env.viewer.camera.positionCartographic.height = 9500;
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const entry = createCctvThumbnailOverlayEntry({
id: 'scaled-leader',
position: position(),
@@ -1930,7 +1935,7 @@ test('CCTV leader remains one CSS pixel at the 0.35 altitude scale point', () =>
test('active CCTV thumbnail is protected outside the ambient quota and excludes ambient footprint', () => {
const env = installMockEnvironment({ width: 800, height: 600, dpr: 1 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const readySlot = () => ({ frame: { ready: true }, stamp: 1 });
const active = createCctvThumbnailOverlayEntry({
id: 'active',
@@ -1967,7 +1972,7 @@ test('active CCTV thumbnail is protected outside the ambient quota and excludes
test('protected tracked entry bypasses ambient quota and excludes its paint footprint', () => {
const env = installMockEnvironment({ width: 800, height: 600, dpr: 1 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const position = new Cesium.Cartesian3(0, 0, 0);
setOverlayEntries('ambient-source', [{
id: 'ambient',
@@ -2010,7 +2015,7 @@ test('protected tracked entry bypasses ambient quota and excludes its paint foot
test('three clustered protected cards fall back to the least-overlapping placement', () => {
const env = installMockEnvironment({ width: 800, height: 600, dpr: 1 });
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const anchor = new Cesium.Cartesian3(0, 0, 0);
setOverlayEntries('protected-cluster', [
selectedEntry('wide-first', {
@@ -2051,7 +2056,7 @@ test('three clustered protected cards fall back to the least-overlapping placeme
test('steady frames reuse paint-item and paint-rectangle pool identities', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('pool-reuse', [selectedEntry('ambient', {
selected: false,
protected: false,
@@ -2078,7 +2083,7 @@ test('steady frames reuse paint-item and paint-rectangle pool identities', () =>
test('destroy empties paint pools and releases pooled record and entry payloads', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('pool-release', [selectedEntry('payload', {
metadata: { retainedProbe: new Uint8Array(1024) },
})]);
@@ -2095,7 +2100,7 @@ test('destroy empties paint pools and releases pooled record and entry payloads'
assert.equal(released.paintItemPoolSize, 0);
assert.equal(released.paintRectPoolSize, 0);
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('pool-release', [selectedEntry('next-payload')]);
env.postRender.raise();
assert.notEqual(getOverlayPaintRect('pool-release', 'next-payload'), firstRect);
@@ -2109,7 +2114,7 @@ function togglablePosition(box) {
test('a candidate that is not projected this frame stops painting immediately', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const box = { hidden: false };
setOverlayEntries('stamp', [selectedEntry('ambient', {
selected: false,
@@ -2136,7 +2141,7 @@ test('a candidate that is not projected this frame stops painting immediately',
test('published paint rectangles are scoped to the publishing key and frame', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const first = { hidden: false };
const second = { hidden: false };
setOverlayEntries('rects', [
@@ -2171,7 +2176,7 @@ test('published paint rectangles are scoped to the publishing key and frame', ()
test('every exported entry point is inert after destroy', () => {
const env = installMockEnvironment();
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('post-destroy', [selectedEntry('live', { interactive: true })]);
env.postRender.raise();
assert.equal(getWorldOverlayDiagnostics().paintedCount, 1);
@@ -2206,7 +2211,7 @@ test('every exported entry point is inert after destroy', () => {
assert.equal(env.window.listenerCount('resize'), 0);
// A later init still produces a fully working host.
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
setOverlayEntries('post-destroy', [selectedEntry('revived', { interactive: true })]);
env.postRender.raise();
assert.equal(getWorldOverlayDiagnostics().paintedCount, 1);
@@ -2246,7 +2251,7 @@ test('installation titles share persistent card arbitration and retain a bounded
overlayHost: { setEntries: setOverlayEntries, clearSource: clearOverlaySource,
setVisible: setOverlaySourceVisible, getDiagnostics: getWorldOverlayDiagnostics },
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
markers.enable();
const records = Array.from({ length: 40 }, (_, i) => ({ id: `site:${i}`, name: `Base ${i}`,
namedArea: true, pointOnly: i % 2 === 0, areaM2: 1000 - i,
@@ -2285,7 +2290,7 @@ test('installation titles share persistent card arbitration and retain a bounded
test('promoting an ambient entry to selected paints it once with no fading duplicate', () => {
const env = installMockEnvironment();
try {
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const entry = { id: 'site', position: position(), title: 'One site', variant: 'card', collisionGroup: 'ambient-card', horizonCull: false, edgeFade: false };
const paint = () => { env.ctx.calls.length = 0; env.postRender.raise(); return env.ctx.calls.filter(([op, text]) => op === 'fillText' && text === 'One site').length; };
setOverlayEntries('military-installations', [entry]); paint(); env.advanceTime(200);
@@ -2364,3 +2369,12 @@ test('on a globe engine the host hands painters the live camera position and req
assert.equal(frame.occluder.isPointVisible(Cesium.Cartesian3.fromDegrees(0, 0, 0)), true);
env.cleanup();
});
test('the host takes a globe engine and refuses a bare viewer', async () => {
const { env, engine } = await fakeEngineHost();
assert.throws(() => initWorldOverlay(env.viewer), /requires a globe engine/);
assert.throws(() => initWorldOverlay(null), /requires a globe engine/);
initWorldOverlay(engine);
assert.equal(engine.frames.postRender.numberOfListeners, 1);
env.cleanup();
});
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
import {
AMBIENT_CARD_COLLISION_CAPACITY,
destroyWorldOverlay,
@@ -316,6 +317,8 @@ function installMockEnvironment({ width, height, dpr }) {
return {
viewer,
// Shell modules take the globe engine; the Cesium engine runs on the mock.
engine: createCesiumEngine(viewer),
postRender,
advanceTime(ms) {
currentTime += ms;
@@ -1174,7 +1177,7 @@ function main() {
height: VIEWPORT_HEIGHT,
dpr: 2,
});
initWorldOverlay(env.viewer);
initWorldOverlay(env.engine);
const workload =
PROFILE === 'phase6-detection'
? buildPhase6DetectionWorkload(ENTRY_COUNT)
@@ -1318,6 +1321,7 @@ function main() {
if (detectionActive) destroyDetection();
destroyWorldOverlay();
env.engine.destroy();
}
main();
+10 -3
View File
@@ -61,11 +61,18 @@ import { runCctvLayerEnableTransition } from '../cctvFocusPolicy.js';
export class StyleManager extends ShellFacade {
/**
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
* @param {object} [options]
* @param {object} [options] `engine`: the page's globe engine (the world
* overlay host runs on it).
*/
constructor(
viewer,
{ mapStackController = null, placeSearch, services, requestServices } = {},
{
engine = null,
mapStackController = null,
placeSearch,
services,
requestServices,
} = {},
) {
super();
const {
@@ -460,7 +467,7 @@ export class StyleManager extends ShellFacade {
// The shared world-overlay host must own its one postRender lane before
// detection and tracked-readout initialize. It stays transparent until a
// production source explicitly registers entries.
initWorldOverlay(viewer);
initWorldOverlay(engine);
// Initialize detection overlay BEFORE style stages so the composite
// stage is first in the post-process pipeline