mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +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. */
|
/** Construct the existing controls and camera presentation. */
|
||||||
export function createApplicationControls({
|
export function createApplicationControls({
|
||||||
scene: { viewer, mapStackController, operations },
|
scene: { viewer, engine, mapStackController, operations },
|
||||||
loaderStatus,
|
loaderStatus,
|
||||||
Controls = StyleManager,
|
Controls = StyleManager,
|
||||||
services,
|
services,
|
||||||
@@ -24,6 +24,8 @@ export function createApplicationControls({
|
|||||||
...catalogControlServices(catalog),
|
...catalogControlServices(catalog),
|
||||||
},
|
},
|
||||||
requestServices: operations.requests,
|
requestServices: operations.requests,
|
||||||
|
// The page's globe engine: the shell's overlay host runs on it.
|
||||||
|
engine,
|
||||||
mapStackController,
|
mapStackController,
|
||||||
placeSearch,
|
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);
|
assert.equal(engineFromViewer(viewer), engine);
|
||||||
for (const cleanup of cleanups.reverse()) cleanup();
|
for (const cleanup of cleanups.reverse()) cleanup();
|
||||||
assert.equal(engine.fake.destroyed, true);
|
assert.equal(engine.fake.destroyed, true);
|
||||||
assert.throws(
|
assert.equal(
|
||||||
() => engineFromViewer(viewer),
|
engineFromViewer(viewer),
|
||||||
/createCesiumEngine needs a viewer/,
|
null,
|
||||||
'no longer registered: a fresh one would be built',
|
'no longer registered, and none is built',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -3,6 +3,7 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
|
||||||
import {
|
import {
|
||||||
countFadingRenderEntries,
|
countFadingRenderEntries,
|
||||||
destroyDetection,
|
destroyDetection,
|
||||||
@@ -247,8 +248,11 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
|||||||
scene: { postRender, requestRender() {} },
|
scene: { postRender, requestRender() {} },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Shell modules take the globe engine; the Cesium engine runs on the mock.
|
||||||
|
const engine = createCesiumEngine(viewer);
|
||||||
return {
|
return {
|
||||||
viewer,
|
viewer,
|
||||||
|
engine,
|
||||||
postRender,
|
postRender,
|
||||||
document,
|
document,
|
||||||
ctx,
|
ctx,
|
||||||
@@ -259,6 +263,7 @@ function installEnvironment({ width = 800, height = 600, dpr = 2, search = '' }
|
|||||||
cleanup() {
|
cleanup() {
|
||||||
destroyDetection();
|
destroyDetection();
|
||||||
destroyWorldOverlay();
|
destroyWorldOverlay();
|
||||||
|
engine.destroy();
|
||||||
Date.now = originalDateNow;
|
Date.now = originalDateNow;
|
||||||
if (originalPath2D === undefined) delete globalThis.Path2D;
|
if (originalPath2D === undefined) delete globalThis.Path2D;
|
||||||
else globalThis.Path2D = originalPath2D;
|
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 env = installEnvironment({ search: '?detectDebug=1' });
|
||||||
const modes = [];
|
const modes = [];
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(env.viewer, [detectableLayer()], (mode) => modes.push(mode));
|
initDetection(env.viewer, [detectableLayer()], (mode) => modes.push(mode));
|
||||||
assert.equal(env.postRender.listeners.size, 1);
|
assert.equal(env.postRender.listeners.size, 1);
|
||||||
assert.equal(env.document.getElementById('detection-overlay'), null);
|
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',
|
type: 'AIR',
|
||||||
}];
|
}];
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(env.viewer, [{ id: 'flights', getDetectableObjects: () => objects }], () => {});
|
initDetection(env.viewer, [{ id: 'flights', getDetectableObjects: () => objects }], () => {});
|
||||||
setMode('DENSE');
|
setMode('DENSE');
|
||||||
env.advance(250);
|
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.
|
// path once per style and reads the fill style the shared canvas received.
|
||||||
const env = installEnvironment();
|
const env = installEnvironment();
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(env.viewer, [mixedTierLayer()], () => {});
|
initDetection(env.viewer, [mixedTierLayer()], () => {});
|
||||||
setMode('DENSE');
|
setMode('DENSE');
|
||||||
settleFrame(env);
|
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.
|
// tiers at once can show it.
|
||||||
const env = installEnvironment();
|
const env = installEnvironment();
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(env.viewer, [mixedTierLayer()], () => {});
|
initDetection(env.viewer, [mixedTierLayer()], () => {});
|
||||||
setDetectionStyle('surveillance');
|
setDetectionStyle('surveillance');
|
||||||
setMode('DENSE');
|
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', () => {
|
test('surveillance military tier reaches the dedicated render target as shipped red', () => {
|
||||||
const env = installEnvironment();
|
const env = installEnvironment();
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(env.viewer, [militaryLayer()], () => {});
|
initDetection(env.viewer, [militaryLayer()], () => {});
|
||||||
setDetectionStyle('surveillance');
|
setDetectionStyle('surveillance');
|
||||||
setMode('DENSE');
|
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.
|
// Opts into the telemetry banner: the held-frame assertions count banner paints.
|
||||||
const env = installEnvironment({ search: '?detectDebug=1' });
|
const env = installEnvironment({ search: '?detectDebug=1' });
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(env.viewer, [detectableLayer()], () => {});
|
initDetection(env.viewer, [detectableLayer()], () => {});
|
||||||
setOverlayEntries('valve-card', [{
|
setOverlayEntries('valve-card', [{
|
||||||
id: 'ambient-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 painted = (search) => {
|
||||||
const env = installEnvironment({ search });
|
const env = installEnvironment({ search });
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(env.viewer, [detectableLayer()], () => {});
|
initDetection(env.viewer, [detectableLayer()], () => {});
|
||||||
setMode('DENSE');
|
setMode('DENSE');
|
||||||
env.advance(250);
|
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.
|
// is the supported way to read them, and is what the QA harnesses use.
|
||||||
const env = installEnvironment();
|
const env = installEnvironment();
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(env.viewer, [detectableLayer()], () => {});
|
initDetection(env.viewer, [detectableLayer()], () => {});
|
||||||
setMode('DENSE');
|
setMode('DENSE');
|
||||||
env.advance(250);
|
env.advance(250);
|
||||||
@@ -793,7 +798,7 @@ test('civilian and military AIR brackets cover front, left, and right at Sparse
|
|||||||
metric: 'FL120',
|
metric: 'FL120',
|
||||||
type: 'AIR',
|
type: 'AIR',
|
||||||
}));
|
}));
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(env.viewer, [{ id: layerId, getDetectableObjects: () => objects }], () => {});
|
initDetection(env.viewer, [{ id: layerId, getDetectableObjects: () => objects }], () => {});
|
||||||
setDetectionTuning({ densityPct: 25 });
|
setDetectionTuning({ densityPct: 25 });
|
||||||
setMode('SPARSE');
|
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.
|
// the shared canvas actually received for each plate.
|
||||||
const env = installEnvironment();
|
const env = installEnvironment();
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(env.viewer, [backdropLayer()], () => {});
|
initDetection(env.viewer, [backdropLayer()], () => {});
|
||||||
setDetectionStyle('normal');
|
setDetectionStyle('normal');
|
||||||
setMode('DENSE');
|
setMode('DENSE');
|
||||||
@@ -909,7 +914,7 @@ test('a transit vehicle is a contact: box, route, mode and operator on the share
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initDetection(
|
initDetection(
|
||||||
env.viewer,
|
env.viewer,
|
||||||
[{ id: 'transit', getDetectableObjects: () => objects }],
|
[{ id: 'transit', getDetectableObjects: () => objects }],
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
// The bridge for shell callers that still hold only a Cesium viewer (layers
|
// 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
|
// until phase 1c): the engine registered for that viewer. The app registers
|
||||||
// app registers its scene engine (src/app/globeEngine.js); any other viewer
|
// its scene engine (src/app/globeEngine.js) and unregisters it before
|
||||||
// gets one built on first use and cached. Building one changes nothing in
|
// destroying it. Nothing is ever built here, so no engine can outlive its
|
||||||
// the viewer, and it is never destroyed here: its subscriptions live only as
|
// viewer without a destroy path: an unregistered or torn-down viewer has none.
|
||||||
// long as their callers keep them.
|
|
||||||
|
|
||||||
import { isGlobeEngine } from '../contract.js';
|
import { isGlobeEngine } from '../contract.js';
|
||||||
import { createCesiumEngine } from './createCesiumEngine.js';
|
|
||||||
|
|
||||||
const engines = new WeakMap();
|
const engines = new WeakMap();
|
||||||
|
|
||||||
@@ -24,17 +22,11 @@ export function registerViewerEngine(viewer, engine) {
|
|||||||
/**
|
/**
|
||||||
* @param {object | null | undefined} viewer A Cesium viewer, or an engine
|
* @param {object | null | undefined} viewer A Cesium viewer, or an engine
|
||||||
* (returned as is).
|
* (returned as is).
|
||||||
* @param {Parameters<typeof createCesiumEngine>[1]} [options] Used only when
|
* @returns {import('../contract.js').GlobeEngine | null} The registered
|
||||||
* an engine has to be built.
|
* engine, or null.
|
||||||
* @returns {import('../contract.js').GlobeEngine | null}
|
|
||||||
*/
|
*/
|
||||||
export function engineFromViewer(viewer, options) {
|
export function engineFromViewer(viewer) {
|
||||||
if (viewer == null) return null;
|
if (viewer == null) return null;
|
||||||
if (isGlobeEngine(viewer)) return viewer;
|
if (isGlobeEngine(viewer)) return viewer;
|
||||||
let engine = engines.get(viewer);
|
return engines.get(viewer) ?? null;
|
||||||
if (!engine) {
|
|
||||||
engine = createCesiumEngine(viewer, options);
|
|
||||||
engines.set(viewer, engine);
|
|
||||||
}
|
|
||||||
return engine;
|
|
||||||
}
|
}
|
||||||
@@ -5,28 +5,25 @@ import { createFakeEngine } from '../testing/fakeEngine.js';
|
|||||||
import { engineFromViewer, registerViewerEngine } from './engineFromViewer.js';
|
import { engineFromViewer, registerViewerEngine } from './engineFromViewer.js';
|
||||||
import { createFakeViewer } from './testing/fakeViewer.mjs';
|
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 viewer = createFakeViewer();
|
||||||
const engine = engineFromViewer(viewer);
|
assert.equal(engineFromViewer(viewer), null);
|
||||||
assert.equal(isGlobeEngine(engine), true);
|
assert.deepEqual(viewer.calls, []);
|
||||||
assert.equal(engine.kind, 'cesium');
|
|
||||||
assert.equal(engineFromViewer(viewer), engine);
|
|
||||||
assert.deepEqual(viewer.calls, [], 'building it changes nothing');
|
|
||||||
assert.notEqual(engineFromViewer(createFakeViewer()), engine);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 viewer = createFakeViewer();
|
||||||
const engine = createFakeEngine();
|
const engine = createFakeEngine();
|
||||||
const unregister = registerViewerEngine(viewer, engine);
|
const unregister = registerViewerEngine(viewer, engine);
|
||||||
assert.equal(engineFromViewer(viewer), engine);
|
assert.equal(engineFromViewer(viewer), engine);
|
||||||
unregister();
|
unregister();
|
||||||
const built = engineFromViewer(viewer);
|
assert.equal(engineFromViewer(viewer), null, 'a torn-down viewer has none');
|
||||||
assert.notEqual(built, engine);
|
const next = createFakeEngine();
|
||||||
|
registerViewerEngine(viewer, next);
|
||||||
unregister();
|
unregister();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
engineFromViewer(viewer),
|
engineFromViewer(viewer),
|
||||||
built,
|
next,
|
||||||
'a stale unregister keeps the new one',
|
'a stale unregister keeps the new one',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import * as geo from '../geo/index.js';
|
import * as geo from '../geo/index.js';
|
||||||
import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
|
import { isGlobeEngine } from '../globe/contract.js';
|
||||||
import {
|
import {
|
||||||
createCyberSonarSampler,
|
createCyberSonarSampler,
|
||||||
isCyberContactSonarActive as isCyberSonarActive,
|
isCyberContactSonarActive as isCyberSonarActive,
|
||||||
@@ -2664,15 +2664,11 @@ function createDevFacade() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the singleton host. Repeated calls for the same engine are no-op.
|
* Initialize the singleton host. Repeated calls for the same engine are no-op.
|
||||||
* @param {import('../globe/contract.js').GlobeEngine | object} target The
|
* @param {import('../globe/contract.js').GlobeEngine} engine
|
||||||
* globe engine, or a Cesium viewer (its engine: engineFromViewer).
|
|
||||||
*/
|
*/
|
||||||
export function initWorldOverlay(target) {
|
export function initWorldOverlay(engine) {
|
||||||
const engine = engineFromViewer(target);
|
if (!isGlobeEngine(engine))
|
||||||
if (!engine)
|
throw new TypeError('initWorldOverlay requires a globe engine');
|
||||||
throw new TypeError(
|
|
||||||
'initWorldOverlay requires a globe engine or a Cesium viewer',
|
|
||||||
);
|
|
||||||
if (_engine === engine && _canvas && _removePostRender) return;
|
if (_engine === engine && _canvas && _removePostRender) return;
|
||||||
if (_engine) destroyWorldOverlay();
|
if (_engine) destroyWorldOverlay();
|
||||||
_destroyed = false;
|
_destroyed = false;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
|
||||||
import {
|
import {
|
||||||
getKeyholeGeometry,
|
getKeyholeGeometry,
|
||||||
keyholeLabelAlphaFromGeometry,
|
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 {
|
return {
|
||||||
viewer,
|
viewer,
|
||||||
|
engine,
|
||||||
ctx,
|
ctx,
|
||||||
detectionCtx,
|
detectionCtx,
|
||||||
paintTrace,
|
paintTrace,
|
||||||
@@ -395,6 +399,7 @@ function installMockEnvironment({
|
|||||||
advanceTime(ms) { currentTime += ms; },
|
advanceTime(ms) { currentTime += ms; },
|
||||||
cleanup() {
|
cleanup() {
|
||||||
destroyWorldOverlay();
|
destroyWorldOverlay();
|
||||||
|
engine.destroy();
|
||||||
delete globalThis.document;
|
delete globalThis.document;
|
||||||
delete globalThis.window;
|
delete globalThis.window;
|
||||||
delete globalThis.ResizeObserver;
|
delete globalThis.ResizeObserver;
|
||||||
@@ -435,7 +440,7 @@ test('destroying before the first init leaves pre-init buffering intact', () =>
|
|||||||
setOverlayEntries('pre-init', [selectedEntry('buffered')]);
|
setOverlayEntries('pre-init', [selectedEntry('buffered')]);
|
||||||
assert.equal(getWorldOverlayDiagnostics().entryCount, 1);
|
assert.equal(getWorldOverlayDiagnostics().entryCount, 1);
|
||||||
|
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
assert.equal(getWorldOverlayDiagnostics().paintedCount, 1);
|
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', () => {
|
test('lifecycle is idempotent and teardown removes listeners, observers, and DOM', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
assert.equal(env.document.getElementById('world-overlay-detection-surface'), null);
|
assert.equal(env.document.getElementById('world-overlay-detection-surface'), null);
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
assert.equal(env.postRender.listeners.size, 1);
|
assert.equal(env.postRender.listeners.size, 1);
|
||||||
assert.equal(env.moveEnd.listeners.size, 1);
|
assert.equal(env.moveEnd.listeners.size, 1);
|
||||||
assert.equal(env.window.listenerCount('gev:cockpit-mode-changed'), 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
|
// changed was containment. This walks the ancestor chain the host actually
|
||||||
// builds and evaluates each node against the shipped stylesheet.
|
// builds and evaluates each node against the shipped stylesheet.
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const css = readStylesheet(new URL('../../style.css', import.meta.url));
|
const css = readStylesheet(new URL('../../style.css', import.meta.url));
|
||||||
const surface = env.document.getElementById('world-overlay-detection-surface');
|
const surface = env.document.getElementById('world-overlay-detection-surface');
|
||||||
assert.ok(surface, 'the host owns a 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', () => {
|
test('canvas backing store tracks CSS size and live DPR', () => {
|
||||||
const env = installMockEnvironment({ width: 400, height: 300, dpr: 2 });
|
const env = installMockEnvironment({ width: 400, height: 300, dpr: 2 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
let canvas = env.document.getElementById('world-overlay-canvas');
|
let canvas = env.document.getElementById('world-overlay-canvas');
|
||||||
let detectionSurface = env.document.getElementById('world-overlay-detection-surface');
|
let detectionSurface = env.document.getElementById('world-overlay-detection-surface');
|
||||||
assert.equal(canvas.width, 0);
|
assert.equal(canvas.width, 0);
|
||||||
@@ -619,7 +624,7 @@ test('shared fade tuning reaches a host-painted card on the next rendered frame'
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
setKeyholeFadeTuning({ fadeRatio: 0.16, outsideOpacity: 0.05 });
|
setKeyholeFadeTuning({ fadeRatio: 0.16, outsideOpacity: 0.05 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('fade-host', [selectedEntry('CARD', {
|
setOverlayEntries('fade-host', [selectedEntry('CARD', {
|
||||||
position: new Cesium.Cartesian3(0.85, 0, 0),
|
position: new Cesium.Cartesian3(0.85, 0, 0),
|
||||||
variant: 'card',
|
variant: 'card',
|
||||||
@@ -649,7 +654,7 @@ test('inlined host alpha binding matches combinedOverlayAlpha across channel ran
|
|||||||
get() { return paintedAlphas.at(-1) ?? 1; },
|
get() { return paintedAlphas.at(-1) ?? 1; },
|
||||||
set(value) { paintedAlphas.push(value); },
|
set(value) { paintedAlphas.push(value); },
|
||||||
});
|
});
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const cases = [
|
const cases = [
|
||||||
{ source: 1, entry: 0.9, temporal: 0.8, distance: 0.25, altitude: 0.25 },
|
{ 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 },
|
{ 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', () => {
|
test('a host dormant since init holds no canvas backing store', () => {
|
||||||
const env = installMockEnvironment({ width: 1600, height: 900, dpr: 2 });
|
const env = installMockEnvironment({ width: 1600, height: 900, dpr: 2 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const canvas = env.document.getElementById('world-overlay-canvas');
|
const canvas = env.document.getElementById('world-overlay-canvas');
|
||||||
const detectionSurface = env.document.getElementById('world-overlay-detection-surface');
|
const detectionSurface = env.document.getElementById('world-overlay-detection-surface');
|
||||||
assert.equal(canvas.width, 0);
|
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', () => {
|
test('empty postRender stays inert through production mutation, resize, and clock noise', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
const settledCallCount = env.ctx.calls.length;
|
const settledCallCount = env.ctx.calls.length;
|
||||||
const settledLayoutReads = { ...env.layoutReads };
|
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', () => {
|
test('HUD mutations defer occluder selector scans until overlay paint work exists', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const settledQueries = { ...env.selectorQueries };
|
const settledQueries = { ...env.selectorQueries };
|
||||||
const hudMutation = {
|
const hudMutation = {
|
||||||
type: 'childList',
|
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 } },
|
{ 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')]);
|
setOverlayEntries('ticker-noise', [selectedEntry('live-entry')]);
|
||||||
env.postRender.raise();
|
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 } },
|
{ id: 'pp-toggles', rect: { left: 300, top: 40, width: 60, height: 200 } },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('chrome-changes', [selectedEntry('live-entry')]);
|
setOverlayEntries('chrome-changes', [selectedEntry('live-entry')]);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
const observer = env.mutationObservers[0];
|
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 } },
|
{ 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')]);
|
setOverlayEntries('observer-scope', [selectedEntry('live-entry')]);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
|
|
||||||
@@ -909,7 +914,7 @@ test('projection records never retain keys for entries that are no longer live',
|
|||||||
|
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
try {
|
try {
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
for (let batch = 0; batch < 4; batch++) {
|
for (let batch = 0; batch < 4; batch++) {
|
||||||
const entries = Array.from({ length: 320 }, (_, index) => (
|
const entries = Array.from({ length: 320 }, (_, index) => (
|
||||||
selectedEntry(`batch-${batch}-${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', () => {
|
test('the per-domain candidate index stays bounded while identities churn', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const COHORT = 40;
|
const COHORT = 40;
|
||||||
const BATCHES = 60;
|
const BATCHES = 60;
|
||||||
const batchEntries = (batch) => Array.from({ length: COHORT }, (_, index) => (
|
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/);
|
assert.throws(() => normalizeOverlayEntry('fires', { id: 'a', position: position(), variant: 'table' }), /variant/);
|
||||||
|
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('fires', [normalized]);
|
setOverlayEntries('fires', [normalized]);
|
||||||
upsertOverlayEntry('fires', { id: 'b', position: position(), variant: 'card' });
|
upsertOverlayEntry('fires', { id: 'b', position: position(), variant: 'card' });
|
||||||
assert.deepEqual(getWorldOverlayDiagnostics().entriesBySource, { fires: 2 });
|
assert.deepEqual(getWorldOverlayDiagnostics().entriesBySource, { fires: 2 });
|
||||||
@@ -1019,7 +1024,7 @@ test('accessible actions announce only accepted focus and expose selected state'
|
|||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
let acceptedActivations = 0;
|
let acceptedActivations = 0;
|
||||||
let staleActivations = 0;
|
let staleActivations = 0;
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('accessible-actions', [selectedEntry('vessel:123', {
|
setOverlayEntries('accessible-actions', [selectedEntry('vessel:123', {
|
||||||
interactive: true,
|
interactive: true,
|
||||||
accessibilityLabel: 'Focus vessel TEST, MMSI 123',
|
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', () => {
|
test('paint lanes are deterministic across all seven binding lanes', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
assert.deepEqual(WORLD_OVERLAY_PAINT_LANES, [
|
assert.deepEqual(WORLD_OVERLAY_PAINT_LANES, [
|
||||||
'detection',
|
'detection',
|
||||||
'ambient-label',
|
'ambient-label',
|
||||||
@@ -1136,7 +1141,7 @@ test('custom detection lane receives the shared host frame and paints below ordi
|
|||||||
dpr: 2,
|
dpr: 2,
|
||||||
occluderRect: { left: 10, top: 20, width: 80, height: 40 },
|
occluderRect: { left: 10, top: 20, width: 80, height: 40 },
|
||||||
});
|
});
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
let capturedFrame = null;
|
let capturedFrame = null;
|
||||||
let paintCount = 0;
|
let paintCount = 0;
|
||||||
const lane = registerWorldOverlayPaintLane('detection', (frame) => {
|
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
|
// host wraps every custom-lane painter in save/try/finally-restore. Pin the
|
||||||
// bracketing structurally (the mock ctx records call order, not state).
|
// bracketing structurally (the mock ctx records call order, not state).
|
||||||
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1 });
|
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const lane = registerWorldOverlayPaintLane('detection', (frame) => {
|
const lane = registerWorldOverlayPaintLane('detection', (frame) => {
|
||||||
frame.ctx.beginPath();
|
frame.ctx.beginPath();
|
||||||
frame.ctx.rect(0, 0, 1, 1);
|
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', () => {
|
test('custom lanes without the detection target paint on the shared canvas', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
let target = null;
|
let target = null;
|
||||||
registerWorldOverlayPaintLane('ambient-label', (frame) => {
|
registerWorldOverlayPaintLane('ambient-label', (frame) => {
|
||||||
target = { surface: frame.surface, ctx: frame.ctx };
|
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 },
|
{ id: 'command-dock', rect: chrome.bottom },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
let uiRects = null;
|
let uiRects = null;
|
||||||
registerWorldOverlayPaintLane('detection', (frame) => {
|
registerWorldOverlayPaintLane('detection', (frame) => {
|
||||||
uiRects = frame.uiRects.slice(0, frame.uiRectCount)
|
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 } },
|
{ id: 'pp-toggles', rect: { left: 0, top: 0, width: 0, height: 0 } },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
let uiRectCount = -1;
|
let uiRectCount = -1;
|
||||||
registerWorldOverlayPaintLane('detection', (frame) => { uiRectCount = frame.uiRectCount; }, {
|
registerWorldOverlayPaintLane('detection', (frame) => { uiRectCount = frame.uiRectCount; }, {
|
||||||
id: 'detection',
|
id: 'detection',
|
||||||
@@ -1363,7 +1368,7 @@ test('chrome that fills the viewport suppresses neither cards nor the detection
|
|||||||
dpr: 1,
|
dpr: 1,
|
||||||
occluderRect: { left: 0, top: 0, width: 400, height: 300 },
|
occluderRect: { left: 0, top: 0, width: 400, height: 300 },
|
||||||
});
|
});
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
let detectionPaints = 0;
|
let detectionPaints = 0;
|
||||||
registerWorldOverlayPaintLane('detection', (frame) => {
|
registerWorldOverlayPaintLane('detection', (frame) => {
|
||||||
detectionPaints++;
|
detectionPaints++;
|
||||||
@@ -1402,7 +1407,7 @@ test('a placement is only ever kept under chrome that composites ABOVE the host'
|
|||||||
|
|
||||||
const paintedUnder = (occluders) => {
|
const paintedUnder = (occluders) => {
|
||||||
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1, occluders });
|
const env = installMockEnvironment({ width: 400, height: 300, dpr: 1, occluders });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('ambient', [selectedEntry('CONTACT')]);
|
setOverlayEntries('ambient', [selectedEntry('CONTACT')]);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
const { paintedCount } = getWorldOverlayDiagnostics();
|
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 } },
|
{ 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 uiRectCount = -1;
|
||||||
let detectionPaints = 0;
|
let detectionPaints = 0;
|
||||||
registerWorldOverlayPaintLane('detection', (frame) => {
|
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 anchors = [500, 540, 580, 620, 660, 700];
|
||||||
const project = (separation) => {
|
const project = (separation) => {
|
||||||
const env = installMockEnvironment({ width: 400, height: 800, dpr: 1 });
|
const env = installMockEnvironment({ width: 400, height: 800, dpr: 1 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('cctv', anchors.map((y, index) => selectedEntry(`cam-${index}`, {
|
setOverlayEntries('cctv', anchors.map((y, index) => selectedEntry(`cam-${index}`, {
|
||||||
position: positionAtScreen(200, y, 400, 800),
|
position: positionAtScreen(200, y, 400, 800),
|
||||||
protected: false,
|
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', () => {
|
test('position getters snapshot once per frame and cockpit-gated sources disappear', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
let getterCalls = 0;
|
let getterCalls = 0;
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('tracked', [selectedEntry('flight', {
|
setOverlayEntries('tracked', [selectedEntry('flight', {
|
||||||
position: () => { getterCalls++; return position(); },
|
position: () => { getterCalls++; return position(); },
|
||||||
interactive: true,
|
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);
|
assert.equal(selectBoundedOverlayCohort([...normalized, protectedEntry], 3).length, 4);
|
||||||
|
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('bulk', normalized, { cohortLimit: 3, collisionCapacity: 2 });
|
setOverlayEntries('bulk', normalized, { cohortLimit: 3, collisionCapacity: 2 });
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
const diagnostics = getWorldOverlayDiagnostics();
|
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', () => {
|
test('FIRMS and vessels enlarge the ambient-card lane under an explicit aggregate budget', () => {
|
||||||
const env = installMockEnvironment({ width: 1600, height: 900, dpr: 1 });
|
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 makeEntries = (sourceOffset, count = 200) => Array.from({ length: count }, (_, index) => {
|
||||||
const slot = index % 160;
|
const slot = index % 160;
|
||||||
const column = slot % 16;
|
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', () => {
|
test('paint rectangles publish from a pool and topmost interactive lookup wins', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('hits', [
|
setOverlayEntries('hits', [
|
||||||
selectedEntry('under', { interactive: true, zIndex: 1, collisionGroup: 'under' }),
|
selectedEntry('under', { interactive: true, zIndex: 1, collisionGroup: 'under' }),
|
||||||
selectedEntry('over', { interactive: true, zIndex: 2, collisionGroup: 'over' }),
|
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', () => {
|
test('CCTV stable frame slots gate ambient chrome and wire the exact host hit rectangle', () => {
|
||||||
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
|
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const slot = createFrameSlot();
|
const slot = createFrameSlot();
|
||||||
const entry = createCctvThumbnailOverlayEntry({
|
const entry = createCctvThumbnailOverlayEntry({
|
||||||
id: 'cam-a',
|
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', () => {
|
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 });
|
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const ambient = createCctvThumbnailOverlayEntry({
|
const ambient = createCctvThumbnailOverlayEntry({
|
||||||
id: 'ambient-top',
|
id: 'ambient-top',
|
||||||
position: new Cesium.Cartesian3(0, 0.9, 0),
|
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
|
// 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.
|
// null it; the below-band twin proves the paint path itself is live.
|
||||||
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
|
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const above = createCctvThumbnailOverlayEntry({
|
const above = createCctvThumbnailOverlayEntry({
|
||||||
id: 'yield-above',
|
id: 'yield-above',
|
||||||
position: new Cesium.Cartesian3(-0.5, 0.9, 0),
|
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; },
|
get() { return paintedAlphas.at(-1) ?? 1; },
|
||||||
set(value) { paintedAlphas.push(value); },
|
set(value) { paintedAlphas.push(value); },
|
||||||
});
|
});
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const entry = createCctvThumbnailOverlayEntry({
|
const entry = createCctvThumbnailOverlayEntry({
|
||||||
id: 'altitude-curve',
|
id: 'altitude-curve',
|
||||||
position: position(),
|
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; },
|
get() { return alphas.at(-1) ?? 1; },
|
||||||
set(value) { alphas.push(value); },
|
set(value) { alphas.push(value); },
|
||||||
});
|
});
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const entry = createCctvThumbnailOverlayEntry({
|
const entry = createCctvThumbnailOverlayEntry({
|
||||||
id: 'scene-presentation', position: position(), title: 'SCENE',
|
id: 'scene-presentation', position: position(), title: 'SCENE',
|
||||||
frameSlot: { frame: { ready: true }, stamp: 1 }, active: true,
|
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', () => {
|
test('CCTV leader remains one CSS pixel at the 0.35 altitude scale point', () => {
|
||||||
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
|
const env = installMockEnvironment({ width: 500, height: 400, dpr: 1 });
|
||||||
env.viewer.camera.positionCartographic.height = 9500;
|
env.viewer.camera.positionCartographic.height = 9500;
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const entry = createCctvThumbnailOverlayEntry({
|
const entry = createCctvThumbnailOverlayEntry({
|
||||||
id: 'scaled-leader',
|
id: 'scaled-leader',
|
||||||
position: position(),
|
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', () => {
|
test('active CCTV thumbnail is protected outside the ambient quota and excludes ambient footprint', () => {
|
||||||
const env = installMockEnvironment({ width: 800, height: 600, dpr: 1 });
|
const env = installMockEnvironment({ width: 800, height: 600, dpr: 1 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const readySlot = () => ({ frame: { ready: true }, stamp: 1 });
|
const readySlot = () => ({ frame: { ready: true }, stamp: 1 });
|
||||||
const active = createCctvThumbnailOverlayEntry({
|
const active = createCctvThumbnailOverlayEntry({
|
||||||
id: 'active',
|
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', () => {
|
test('protected tracked entry bypasses ambient quota and excludes its paint footprint', () => {
|
||||||
const env = installMockEnvironment({ width: 800, height: 600, dpr: 1 });
|
const env = installMockEnvironment({ width: 800, height: 600, dpr: 1 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const position = new Cesium.Cartesian3(0, 0, 0);
|
const position = new Cesium.Cartesian3(0, 0, 0);
|
||||||
setOverlayEntries('ambient-source', [{
|
setOverlayEntries('ambient-source', [{
|
||||||
id: 'ambient',
|
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', () => {
|
test('three clustered protected cards fall back to the least-overlapping placement', () => {
|
||||||
const env = installMockEnvironment({ width: 800, height: 600, dpr: 1 });
|
const env = installMockEnvironment({ width: 800, height: 600, dpr: 1 });
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const anchor = new Cesium.Cartesian3(0, 0, 0);
|
const anchor = new Cesium.Cartesian3(0, 0, 0);
|
||||||
setOverlayEntries('protected-cluster', [
|
setOverlayEntries('protected-cluster', [
|
||||||
selectedEntry('wide-first', {
|
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', () => {
|
test('steady frames reuse paint-item and paint-rectangle pool identities', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('pool-reuse', [selectedEntry('ambient', {
|
setOverlayEntries('pool-reuse', [selectedEntry('ambient', {
|
||||||
selected: false,
|
selected: false,
|
||||||
protected: 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', () => {
|
test('destroy empties paint pools and releases pooled record and entry payloads', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('pool-release', [selectedEntry('payload', {
|
setOverlayEntries('pool-release', [selectedEntry('payload', {
|
||||||
metadata: { retainedProbe: new Uint8Array(1024) },
|
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.paintItemPoolSize, 0);
|
||||||
assert.equal(released.paintRectPoolSize, 0);
|
assert.equal(released.paintRectPoolSize, 0);
|
||||||
|
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('pool-release', [selectedEntry('next-payload')]);
|
setOverlayEntries('pool-release', [selectedEntry('next-payload')]);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
assert.notEqual(getOverlayPaintRect('pool-release', 'next-payload'), firstRect);
|
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', () => {
|
test('a candidate that is not projected this frame stops painting immediately', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const box = { hidden: false };
|
const box = { hidden: false };
|
||||||
setOverlayEntries('stamp', [selectedEntry('ambient', {
|
setOverlayEntries('stamp', [selectedEntry('ambient', {
|
||||||
selected: false,
|
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', () => {
|
test('published paint rectangles are scoped to the publishing key and frame', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const first = { hidden: false };
|
const first = { hidden: false };
|
||||||
const second = { hidden: false };
|
const second = { hidden: false };
|
||||||
setOverlayEntries('rects', [
|
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', () => {
|
test('every exported entry point is inert after destroy', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('post-destroy', [selectedEntry('live', { interactive: true })]);
|
setOverlayEntries('post-destroy', [selectedEntry('live', { interactive: true })]);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
assert.equal(getWorldOverlayDiagnostics().paintedCount, 1);
|
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);
|
assert.equal(env.window.listenerCount('resize'), 0);
|
||||||
|
|
||||||
// A later init still produces a fully working host.
|
// A later init still produces a fully working host.
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
setOverlayEntries('post-destroy', [selectedEntry('revived', { interactive: true })]);
|
setOverlayEntries('post-destroy', [selectedEntry('revived', { interactive: true })]);
|
||||||
env.postRender.raise();
|
env.postRender.raise();
|
||||||
assert.equal(getWorldOverlayDiagnostics().paintedCount, 1);
|
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,
|
overlayHost: { setEntries: setOverlayEntries, clearSource: clearOverlaySource,
|
||||||
setVisible: setOverlaySourceVisible, getDiagnostics: getWorldOverlayDiagnostics },
|
setVisible: setOverlaySourceVisible, getDiagnostics: getWorldOverlayDiagnostics },
|
||||||
});
|
});
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
markers.enable();
|
markers.enable();
|
||||||
const records = Array.from({ length: 40 }, (_, i) => ({ id: `site:${i}`, name: `Base ${i}`,
|
const records = Array.from({ length: 40 }, (_, i) => ({ id: `site:${i}`, name: `Base ${i}`,
|
||||||
namedArea: true, pointOnly: i % 2 === 0, areaM2: 1000 - 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', () => {
|
test('promoting an ambient entry to selected paints it once with no fading duplicate', () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
try {
|
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 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; };
|
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);
|
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);
|
assert.equal(frame.occluder.isPointVisible(Cesium.Cartesian3.fromDegrees(0, 0, 0)), true);
|
||||||
env.cleanup();
|
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 * as Cesium from 'cesium';
|
||||||
|
import { createCesiumEngine } from '../globe/cesium/createCesiumEngine.js';
|
||||||
import {
|
import {
|
||||||
AMBIENT_CARD_COLLISION_CAPACITY,
|
AMBIENT_CARD_COLLISION_CAPACITY,
|
||||||
destroyWorldOverlay,
|
destroyWorldOverlay,
|
||||||
@@ -316,6 +317,8 @@ function installMockEnvironment({ width, height, dpr }) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
viewer,
|
viewer,
|
||||||
|
// Shell modules take the globe engine; the Cesium engine runs on the mock.
|
||||||
|
engine: createCesiumEngine(viewer),
|
||||||
postRender,
|
postRender,
|
||||||
advanceTime(ms) {
|
advanceTime(ms) {
|
||||||
currentTime += ms;
|
currentTime += ms;
|
||||||
@@ -1174,7 +1177,7 @@ function main() {
|
|||||||
height: VIEWPORT_HEIGHT,
|
height: VIEWPORT_HEIGHT,
|
||||||
dpr: 2,
|
dpr: 2,
|
||||||
});
|
});
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.engine);
|
||||||
const workload =
|
const workload =
|
||||||
PROFILE === 'phase6-detection'
|
PROFILE === 'phase6-detection'
|
||||||
? buildPhase6DetectionWorkload(ENTRY_COUNT)
|
? buildPhase6DetectionWorkload(ENTRY_COUNT)
|
||||||
@@ -1318,6 +1321,7 @@ function main() {
|
|||||||
|
|
||||||
if (detectionActive) destroyDetection();
|
if (detectionActive) destroyDetection();
|
||||||
destroyWorldOverlay();
|
destroyWorldOverlay();
|
||||||
|
env.engine.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
@@ -61,11 +61,18 @@ import { runCctvLayerEnableTransition } from '../cctvFocusPolicy.js';
|
|||||||
export class StyleManager extends ShellFacade {
|
export class StyleManager extends ShellFacade {
|
||||||
/**
|
/**
|
||||||
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
|
* @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(
|
constructor(
|
||||||
viewer,
|
viewer,
|
||||||
{ mapStackController = null, placeSearch, services, requestServices } = {},
|
{
|
||||||
|
engine = null,
|
||||||
|
mapStackController = null,
|
||||||
|
placeSearch,
|
||||||
|
services,
|
||||||
|
requestServices,
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
const {
|
const {
|
||||||
@@ -460,7 +467,7 @@ export class StyleManager extends ShellFacade {
|
|||||||
// The shared world-overlay host must own its one postRender lane before
|
// The shared world-overlay host must own its one postRender lane before
|
||||||
// detection and tracked-readout initialize. It stays transparent until a
|
// detection and tracked-readout initialize. It stays transparent until a
|
||||||
// production source explicitly registers entries.
|
// production source explicitly registers entries.
|
||||||
initWorldOverlay(viewer);
|
initWorldOverlay(engine);
|
||||||
|
|
||||||
// Initialize detection overlay BEFORE style stages so the composite
|
// Initialize detection overlay BEFORE style stages so the composite
|
||||||
// stage is first in the post-process pipeline
|
// stage is first in the post-process pipeline
|
||||||
|
|||||||
Reference in new issue
Block a user