mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge branch 'main' into upstream/terrain-partial-heights
This commit is contained in:
8 files changed
+212
-3
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
- Pause the HUD's camera readout while the page is hidden, as polled layers
|
||||||
|
already do. An app embedded inline in a panel keeps both updating, because the
|
||||||
|
panel's host can report the page as hidden while it is on screen (nu-stats,
|
||||||
|
#1344).
|
||||||
@@ -798,6 +798,11 @@ rendered detection set. The legacy manager retains panel helper compatibility;
|
|||||||
registration sealing, restoration order and failed-transition rollback remain
|
registration sealing, restoration order and failed-transition rollback remain
|
||||||
unchanged.
|
unchanged.
|
||||||
|
|
||||||
|
Polled layer refreshes and the HUD's camera readout pause while the page is
|
||||||
|
hidden. An app embedded inline in a panel keeps both running, because the
|
||||||
|
panel's host can report the page as hidden while it is on screen. This is the
|
||||||
|
same rule the render loop follows (`keepPanelRendering` in `app/embed.js`).
|
||||||
|
|
||||||
Application scene setup owns request services, terrain/floor caches and annotation
|
Application scene setup owns request services, terrain/floor caches and annotation
|
||||||
lookup state. Controls, layers and voice share those owners, and cancellation
|
lookup state. Controls, layers and voice share those owners, and cancellation
|
||||||
clears caches and removes map-stack listeners. Standalone setup supplies the
|
clears caches and removes map-stack listeners. Standalone setup supplies the
|
||||||
|
|||||||
@@ -94,6 +94,35 @@ test('data setup seals the caller catalog before controls can restore and drains
|
|||||||
assert.equal(dataManager.layers.size, 0);
|
assert.equal(dataManager.layers.size, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('data setup keeps layers polling in an inline panel its host reports hidden', async (t) => {
|
||||||
|
const previous = globalThis.document;
|
||||||
|
globalThis.document = {
|
||||||
|
hidden: true,
|
||||||
|
visibilityState: 'hidden',
|
||||||
|
getElementById: () => null,
|
||||||
|
addEventListener() {},
|
||||||
|
removeEventListener() {},
|
||||||
|
};
|
||||||
|
t.after(() => {
|
||||||
|
delete globalThis.GEV_EMBED_INLINE;
|
||||||
|
if (previous === undefined) delete globalThis.document;
|
||||||
|
else globalThis.document = previous;
|
||||||
|
});
|
||||||
|
const releases = [];
|
||||||
|
const { dataManager } = createApplicationData({
|
||||||
|
scene: { viewer: {} },
|
||||||
|
controls: { styleManager: { attachDataManager() {} } },
|
||||||
|
catalog: createLayerCatalog([], []),
|
||||||
|
defer: (release) => releases.push(release),
|
||||||
|
});
|
||||||
|
const visibility = dataManager._visibilityTarget;
|
||||||
|
assert.equal(visibility.hidden, true, 'a hidden page pauses outside a panel');
|
||||||
|
globalThis.GEV_EMBED_INLINE = true;
|
||||||
|
assert.equal(visibility.hidden, false);
|
||||||
|
assert.equal(visibility.visibilityState, 'visible');
|
||||||
|
for (const release of releases.reverse()) await release();
|
||||||
|
});
|
||||||
|
|
||||||
test('controls bind catalog instances rather than similarly named defaults', () => {
|
test('controls bind catalog instances rather than similarly named defaults', () => {
|
||||||
const ids = [
|
const ids = [
|
||||||
'traffic',
|
'traffic',
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,7 @@
|
|||||||
import { LayerLifecycle } from '../data/lifecycle.js';
|
import { LayerLifecycle } from '../data/lifecycle.js';
|
||||||
import { LayerPresentation } from './layerPresentation.js';
|
import { LayerPresentation } from './layerPresentation.js';
|
||||||
import { createCyberSonarScene } from '../cyberSonarScene.js';
|
import { createCyberSonarScene } from '../cyberSonarScene.js';
|
||||||
|
import { panelAwareVisibility } from './embed.js';
|
||||||
/** Register the application layer catalog before allowing state restoration. */
|
/** Register the application layer catalog before allowing state restoration. */
|
||||||
export function createApplicationData({
|
export function createApplicationData({
|
||||||
scene: { viewer, mapStackController },
|
scene: { viewer, mapStackController },
|
||||||
@@ -13,7 +14,9 @@ export function createApplicationData({
|
|||||||
// Initialize data layer manager
|
// Initialize data layer manager
|
||||||
const dataManager = new LayerLifecycle(viewer, {
|
const dataManager = new LayerLifecycle(viewer, {
|
||||||
allowQaRegistration,
|
allowQaRegistration,
|
||||||
visibilityTarget: document,
|
// A panel's host may report it hidden while it is on screen; its layers
|
||||||
|
// keep polling (see keepPanelRendering in embed.js).
|
||||||
|
visibilityTarget: panelAwareVisibility(document),
|
||||||
getSourceAvailability: catalog?.getSourceAvailability,
|
getSourceAvailability: catalog?.getSourceAvailability,
|
||||||
});
|
});
|
||||||
defer(async () => {
|
defer(async () => {
|
||||||
|
|||||||
@@ -45,6 +45,28 @@ export function isEmbeddedInline() {
|
|||||||
return globalThis.GEV_EMBED_INLINE === true;
|
return globalThis.GEV_EMBED_INLINE === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `doc`'s visibility for work that pauses in a hidden page. A panel's host
|
||||||
|
* may report it hidden while it is on screen (see keepPanelRendering), so an
|
||||||
|
* inline panel always reads as visible.
|
||||||
|
*/
|
||||||
|
export function panelAwareVisibility(doc = globalThis.document) {
|
||||||
|
const hidden = () =>
|
||||||
|
(doc.hidden === true || doc.visibilityState === 'hidden') &&
|
||||||
|
!isEmbeddedInline();
|
||||||
|
return {
|
||||||
|
get hidden() {
|
||||||
|
return hidden();
|
||||||
|
},
|
||||||
|
get visibilityState() {
|
||||||
|
return hidden() ? 'hidden' : 'visible';
|
||||||
|
},
|
||||||
|
addEventListener: (type, listener) => doc.addEventListener(type, listener),
|
||||||
|
removeEventListener: (type, listener) =>
|
||||||
|
doc.removeEventListener(type, listener),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// A panel keeps drawing at about this rate when its host stops animation
|
// A panel keeps drawing at about this rate when its host stops animation
|
||||||
// frames; see keepPanelRendering.
|
// frames; see keepPanelRendering.
|
||||||
const PANEL_FRAME_MS = 33;
|
const PANEL_FRAME_MS = 33;
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
installViews,
|
installViews,
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
keepPanelRendering,
|
keepPanelRendering,
|
||||||
|
panelAwareVisibility,
|
||||||
} from './embed.js';
|
} from './embed.js';
|
||||||
|
import { LayerLifecycle } from '../data/lifecycle.js';
|
||||||
import { createView } from '../view/index.js';
|
import { createView } from '../view/index.js';
|
||||||
|
|
||||||
const fakeViewer = () => {
|
const fakeViewer = () => {
|
||||||
@@ -211,6 +213,60 @@ test('an app loaded inline into a panel page talks through its own window', asyn
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('polled layers keep refreshing in an inline panel its host reports hidden', async (t) => {
|
||||||
|
const listeners = new Set();
|
||||||
|
const doc = {
|
||||||
|
hidden: true,
|
||||||
|
visibilityState: 'hidden',
|
||||||
|
addEventListener: (_type, fn) => listeners.add(fn),
|
||||||
|
removeEventListener: (_type, fn) => listeners.delete(fn),
|
||||||
|
};
|
||||||
|
const settle = async () => {
|
||||||
|
for (let i = 0; i < 3; i++) await Promise.resolve();
|
||||||
|
};
|
||||||
|
t.mock.timers.enable({ apis: ['setInterval', 'Date'], now: 1000 });
|
||||||
|
let updates = 0;
|
||||||
|
const manager = new LayerLifecycle(
|
||||||
|
{},
|
||||||
|
{ visibilityTarget: panelAwareVisibility(doc) },
|
||||||
|
);
|
||||||
|
manager.register({
|
||||||
|
id: 'flights',
|
||||||
|
name: 'flights',
|
||||||
|
icon: '',
|
||||||
|
source: 'test',
|
||||||
|
updateInterval: 1000,
|
||||||
|
async init() {},
|
||||||
|
enable() {},
|
||||||
|
disable() {},
|
||||||
|
async update() {
|
||||||
|
updates++;
|
||||||
|
await Promise.resolve();
|
||||||
|
},
|
||||||
|
getStats: () => ({ count: 0, lastUpdate: null }),
|
||||||
|
});
|
||||||
|
globalThis.GEV_EMBED_INLINE = true;
|
||||||
|
try {
|
||||||
|
await manager.setEnabled('flights', true);
|
||||||
|
await settle();
|
||||||
|
const enabled = updates;
|
||||||
|
t.mock.timers.tick(1000);
|
||||||
|
await settle();
|
||||||
|
t.mock.timers.tick(1000);
|
||||||
|
await settle();
|
||||||
|
assert.equal(updates, enabled + 2, 'the panel keeps polling');
|
||||||
|
|
||||||
|
delete globalThis.GEV_EMBED_INLINE;
|
||||||
|
t.mock.timers.tick(1000);
|
||||||
|
await settle();
|
||||||
|
assert.equal(updates, enabled + 2, 'outside a panel a hidden page pauses');
|
||||||
|
} finally {
|
||||||
|
delete globalThis.GEV_EMBED_INLINE;
|
||||||
|
await manager.destroyAll();
|
||||||
|
}
|
||||||
|
assert.equal(listeners.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('annotations in a link are drawn after it restores, embedded or not', async () => {
|
test('annotations in a link are drawn after it restores, embedded or not', async () => {
|
||||||
const { calls, run } = recorder();
|
const { calls, run } = recorder();
|
||||||
const an = encodeURIComponent(
|
const an = encodeURIComponent(
|
||||||
|
|||||||
+5
-2
@@ -39,6 +39,7 @@ import {
|
|||||||
} from './hudLayouts.js';
|
} from './hudLayouts.js';
|
||||||
import { formatNumber, msg, t } from './i18n/index.js';
|
import { formatNumber, msg, t } from './i18n/index.js';
|
||||||
import { localizeLayerName } from './ui/layerPanel.js';
|
import { localizeLayerName } from './ui/layerPanel.js';
|
||||||
|
import { isEmbeddedInline } from './app/embed.js';
|
||||||
|
|
||||||
/** Color palettes keyed by shader mode; applied as CSS custom properties. */
|
/** Color palettes keyed by shader mode; applied as CSS custom properties. */
|
||||||
const HUD_COLORS = {
|
const HUD_COLORS = {
|
||||||
@@ -363,9 +364,11 @@ export class IntelHUD {
|
|||||||
dot.style.visibility = this._recBlinkState ? 'visible' : 'hidden';
|
dot.style.visibility = this._recBlinkState ? 'visible' : 'hidden';
|
||||||
}, 800);
|
}, 800);
|
||||||
|
|
||||||
// Camera-derived data — 4 updates/second (250ms)
|
// Camera-derived data — 4 updates/second (250ms). Skipped in a hidden
|
||||||
|
// page, except an inline panel, whose host may report it hidden while it
|
||||||
|
// is on screen (see keepPanelRendering in app/embed.js).
|
||||||
this._updateInterval = setInterval(() => {
|
this._updateInterval = setInterval(() => {
|
||||||
if (!this._visible) return;
|
if (!this._visible || (document.hidden && !isEmbeddedInline())) return;
|
||||||
this._updateCameraData();
|
this._updateCameraData();
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// The HUD's 250 ms telemetry readout skips ticks in a hidden page, except in
|
||||||
|
// an inline panel, whose host may report it hidden while it is on screen.
|
||||||
|
//
|
||||||
|
// hud.js imports `mgrs`, which Node's ESM loader cannot read named exports
|
||||||
|
// from, so a module hook swaps it for a stub before the import (the same
|
||||||
|
// setup as hudAltitudeDatum.test.mjs).
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { registerHooks } from 'node:module';
|
||||||
|
|
||||||
|
const MGRS_STUB_URL = 'gev-test-stub:mgrs';
|
||||||
|
registerHooks({
|
||||||
|
resolve(specifier, context, next) {
|
||||||
|
if (specifier === 'mgrs') return { url: MGRS_STUB_URL, shortCircuit: true };
|
||||||
|
return next(specifier, context);
|
||||||
|
},
|
||||||
|
load(url, context, next) {
|
||||||
|
if (url === MGRS_STUB_URL) {
|
||||||
|
return {
|
||||||
|
format: 'module',
|
||||||
|
shortCircuit: true,
|
||||||
|
source: 'export function forward() { return "10SEG55776339"; }\nexport default { forward };\n',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return next(url, context);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { IntelHUD } = await import('./hud.js');
|
||||||
|
|
||||||
|
function installHiddenPage() {
|
||||||
|
const elements = new Map(
|
||||||
|
['hud-alt', 'hud-summary', 'hud-mgrs', 'hud-latlon', 'hud-bottom-line', 'hud-gsd', 'hud-coll', 'hud-ona', 'hud-mode']
|
||||||
|
.map((id) => [id, { textContent: '' }]),
|
||||||
|
);
|
||||||
|
const previousDocument = globalThis.document;
|
||||||
|
globalThis.document = {
|
||||||
|
hidden: true,
|
||||||
|
visibilityState: 'hidden',
|
||||||
|
getElementById: (id) => elements.get(id) ?? null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
addEventListener() {},
|
||||||
|
removeEventListener() {},
|
||||||
|
};
|
||||||
|
const viewer = {
|
||||||
|
camera: {
|
||||||
|
pitch: -Math.PI / 2,
|
||||||
|
positionCartographic: { latitude: 0.7, longitude: -1.2, height: 500 },
|
||||||
|
computeViewRectangle: () => undefined,
|
||||||
|
moveStart: { addEventListener() {}, removeEventListener() {} },
|
||||||
|
moveEnd: { addEventListener() {}, removeEventListener() {} },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
alt: () => elements.get('hud-alt').textContent,
|
||||||
|
viewer,
|
||||||
|
restore() {
|
||||||
|
if (previousDocument === undefined) delete globalThis.document;
|
||||||
|
else globalThis.document = previousDocument;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('the HUD readout pauses in a hidden page but keeps updating in an inline panel', (t) => {
|
||||||
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
||||||
|
const page = installHiddenPage();
|
||||||
|
let hud;
|
||||||
|
try {
|
||||||
|
hud = new IntelHUD(page.viewer);
|
||||||
|
hud._visible = true;
|
||||||
|
|
||||||
|
t.mock.timers.tick(250);
|
||||||
|
assert.equal(page.alt(), '', 'a hidden page skips the telemetry tick');
|
||||||
|
|
||||||
|
globalThis.GEV_EMBED_INLINE = true;
|
||||||
|
t.mock.timers.tick(250);
|
||||||
|
assert.match(
|
||||||
|
page.alt(),
|
||||||
|
/^ALT: 500m/,
|
||||||
|
'an inline panel reported hidden keeps its readout updating',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
delete globalThis.GEV_EMBED_INLINE;
|
||||||
|
hud?.destroy();
|
||||||
|
page.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user