mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
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:
1 parent
3e3aa0bd9f
commit
12c9651a67
10 files changed
+145
-105
No files matched your search
Vendored
+3
-1
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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\)/);
|
||||
});
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
@@ -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 }],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user