mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
feat(hud): render the Intel HUD from a store
IntelHUD no longer builds #intel-hud with innerHTML or writes its readouts by id: it publishes its readouts, typing summary, REC blink, visibility, layout variant and style colors through its publish option to the gev-hud store, and the intel-hud application view renders them. The corners render from mount so the panel layout controllers register them as obstacles. The vessel layer presents the selected vessel's readout through src/hudReadouts.js, which IntelHUD follows, instead of writing into the HUD. The Cyber sonar state answers a HUD toggle from IntelHUD's visibility rather than the class the view renders on its next update.
This commit is contained in:
1 parent
b96cc37ab5
commit
c03b4a7643
26 files changed
+556
-243
No files matched your search
@@ -58,6 +58,7 @@ import { useMissionReplayStore } from './stores/missionReplay.js';
|
||||
import { useCelestialRingStore } from './stores/celestialRing.js';
|
||||
import { useWorldOverlayActionsStore } from './stores/worldOverlayActions.js';
|
||||
import { useAwarenessDirectionStore } from './stores/awarenessDirection.js';
|
||||
import { useHudStore } from './stores/hud.js';
|
||||
import { useDisplayStore } from './stores/display.js';
|
||||
import { useLocaleStore } from './stores/locale.js';
|
||||
import { useLocationStore } from './stores/location.js';
|
||||
@@ -347,9 +348,11 @@ export class StyleManager extends ShellFacade {
|
||||
this._lastTrafficChipUpdateAt = 0;
|
||||
|
||||
// Intel HUD
|
||||
const hudStore = useHudStore(applicationPinia());
|
||||
this.hud = new IntelHUD(viewer, {
|
||||
placeSearch,
|
||||
summaryService: requestServices?.summary,
|
||||
publish: (view) => hudStore.publish(view),
|
||||
});
|
||||
this._recording.hud = this.hud;
|
||||
this._cockpitCoordinator = new CockpitCoordinator({
|
||||
@@ -1489,7 +1492,9 @@ export class StyleManager extends ShellFacade {
|
||||
hud: {
|
||||
visible: !!this.hud?.visible,
|
||||
layout: this.hud?.getVariant?.() || null,
|
||||
sonar: getCyberSonarControlState(),
|
||||
sonar: getCyberSonarControlState(globalThis.document, {
|
||||
hudActive: this.hud ? !!this.hud.visible : undefined,
|
||||
}),
|
||||
},
|
||||
detection: this.getDetectionState(),
|
||||
bloom: {
|
||||
|
||||
@@ -13,14 +13,23 @@ const SETTING_FIELDS = Object.freeze({
|
||||
sectorDeg: ['sector', 8, 60],
|
||||
});
|
||||
|
||||
/** Read configuration separately from whether map effects can currently run. */
|
||||
export function getCyberSonarControlState(doc = globalThis.document) {
|
||||
/**
|
||||
* Read configuration separately from whether map effects can currently run.
|
||||
* @param {Document} [doc]
|
||||
* @param {{hudActive?: boolean}} [state] The HUD's visibility from IntelHUD,
|
||||
* which the view renders only on its next update.
|
||||
*/
|
||||
export function getCyberSonarControlState(
|
||||
doc = globalThis.document,
|
||||
{ hudActive } = {},
|
||||
) {
|
||||
const root = doc?.documentElement;
|
||||
const settings = readCyberSonarSettings(root);
|
||||
const options = {
|
||||
root,
|
||||
body: doc?.body,
|
||||
hud: doc?.getElementById?.('intel-hud') ?? null,
|
||||
hudActive,
|
||||
};
|
||||
return {
|
||||
enabled: isCyberSonarEnabled(root),
|
||||
@@ -47,7 +56,7 @@ export function setCyberSonarControls(
|
||||
const fail = (error) => ({
|
||||
ok: false,
|
||||
error,
|
||||
sonar: getCyberSonarControlState(doc),
|
||||
sonar: getCyberSonarControlState(doc, { hudActive: owner?.hud?.visible }),
|
||||
});
|
||||
if (!request || typeof request !== 'object' || Array.isArray(request)) {
|
||||
return fail('Provide Cyber sonar settings as an object.');
|
||||
@@ -82,5 +91,8 @@ export function setCyberSonarControls(
|
||||
if (Object.hasOwn(request, 'enabled')) {
|
||||
owner._setCyberSonarEnabled(request.enabled);
|
||||
}
|
||||
return { ok: true, sonar: getCyberSonarControlState(doc) };
|
||||
return {
|
||||
ok: true,
|
||||
sonar: getCyberSonarControlState(doc, { hudActive: owner?.hud?.visible }),
|
||||
};
|
||||
}
|
||||
@@ -161,3 +161,19 @@ test('reports inactive map effects in Cockpit, hidden HUD, unsupported and unini
|
||||
assert.equal(getCyberSonarControlState(f.doc).contactSweepActive, false);
|
||||
}
|
||||
});
|
||||
|
||||
test('the HUD visibility its owner reports answers before the view re-renders', () => {
|
||||
const f = fixture();
|
||||
f.root.dataset.cyberSonar = 'on';
|
||||
// The HUD was just hidden: IntelHUD knows, the rendered class does not yet.
|
||||
assert.equal(getCyberSonarControlState(f.doc).mapSweepActive, true);
|
||||
assert.equal(
|
||||
getCyberSonarControlState(f.doc, { hudActive: false }).mapSweepActive,
|
||||
false,
|
||||
);
|
||||
f.owner.hud.visible = false;
|
||||
const result = setCyberSonarControls(f.owner, { rings: 6 }, f.doc);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.sonar.mapSweepActive, false);
|
||||
assert.equal(result.sonar.contactSweepActive, false);
|
||||
});
|
||||
@@ -651,6 +651,7 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
||||
const stylesheet = read('../../style.css');
|
||||
const rightRail = read('./rightPanelRail.js');
|
||||
const hud = read('../hud.js');
|
||||
const hudView = read('./views/IntelHudView.vue');
|
||||
const detection = read('../data/detection.js');
|
||||
const civilRendering = read('../layers/flights/rendering.js');
|
||||
const militaryRendering = read('../layers/military/rendering.js');
|
||||
@@ -680,7 +681,7 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
||||
/\.panel-collapsible:not\(#location-bar\):not\(#control-panel\)::after/,
|
||||
);
|
||||
assert.match(cyberStyles, /clip-path: var\(--cyber-panel-frame\);/);
|
||||
assert.match(hud, /class="hud-sonar" aria-hidden="true"/);
|
||||
assert.match(hudView, /class="hud-sonar" aria-hidden="true"/);
|
||||
assert.match(cyberStyles, /\.hud-sonar::before/);
|
||||
assert.match(cyberStyles, /\.hud-sonar::after/);
|
||||
assert.match(cyberStyles, /@keyframes cyber-sonar-sweep/);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { shallowRef } from 'vue';
|
||||
|
||||
/**
|
||||
* The Intel HUD as its view renders it. IntelHUD presents its readouts,
|
||||
* visibility, layout variant and style colors and the application shell
|
||||
* publishes them here.
|
||||
*/
|
||||
export const useHudStore = defineStore('gev-hud', () => {
|
||||
/** The presented HUD, frozen by IntelHUD; null until it is built. */
|
||||
const hud = shallowRef(null);
|
||||
|
||||
/** @param {?object} next A fresh presentation; every publish re-renders. */
|
||||
function publish(next) {
|
||||
hud.value = next;
|
||||
}
|
||||
|
||||
return { hud, publish };
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useHudStore } from '../stores/hud.js';
|
||||
|
||||
// The readouts, visibility, layout variant and colors IntelHUD publishes.
|
||||
const { hud } = storeToRefs(useHudStore());
|
||||
// The corners and edges render from mount, before IntelHUD presents them:
|
||||
// the panel layout controllers register them as obstacles synchronously,
|
||||
// right after IntelHUD is built.
|
||||
const EMPTY = Object.freeze({ vessel: {}, sensor: [], recShown: null });
|
||||
const view = computed(() => hud.value ?? EMPTY);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="intel-hud" :class="{ active: hud?.active }" :data-variant="hud?.variant" :style="hud?.colors ?? undefined">
|
||||
<div class="hud-sonar" aria-hidden="true"></div>
|
||||
<div class="hud-corner hud-top-left">
|
||||
<div class="hud-bracket">┌</div>
|
||||
<div class="hud-content">
|
||||
<div class="hud-readout"><div class="hud-classification">{{ view.classification }}</div></div>
|
||||
<div class="hud-readout"><div class="hud-system">{{ view.system }}</div></div>
|
||||
<div class="hud-readout"><div class="hud-mode" id="hud-mode">{{ view.mode }}</div></div>
|
||||
<div class="hud-summary-wrap">
|
||||
<div class="hud-readout"><div class="hud-summary-label">{{ view.summaryLabel }}</div></div>
|
||||
<div class="hud-readout"><div class="hud-summary" id="hud-summary">{{ view.summary }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hud-corner hud-top-right">
|
||||
<div class="hud-content" style="text-align: right">
|
||||
<div class="hud-readout"><div class="hud-rec"><span id="hud-rec-dot" :style="view.recShown === null ? undefined : { visibility: view.recShown ? 'visible' : 'hidden' }">●</span>{{ view.rec ? ` ${view.rec} ` : '' }}<span id="hud-timestamp">{{ view.timestamp }}</span></div></div>
|
||||
<div class="hud-readout"><div class="hud-orbital">{{ view.orbital }}</div></div>
|
||||
</div>
|
||||
<div class="hud-bracket">┐</div>
|
||||
</div>
|
||||
<div class="hud-corner hud-bottom-left">
|
||||
<div class="hud-bracket">└</div>
|
||||
<div class="hud-content">
|
||||
<div class="hud-readout"><div id="hud-mgrs">{{ view.mgrs }}</div></div>
|
||||
<div class="hud-readout"><div id="hud-latlon">{{ view.latlon }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hud-corner hud-bottom-right">
|
||||
<div class="hud-content" style="text-align: right">
|
||||
<div class="hud-readout"><div id="hud-gsd">{{ view.gsd }}</div></div>
|
||||
<div class="hud-readout"><div id="hud-alt">{{ view.alt }}</div></div>
|
||||
<div class="hud-readout"><div id="hud-ais-vessel" :class="['hud-ais-vessel', { active: view.vessel.active }]">{{ view.vessel.text }}</div></div>
|
||||
</div>
|
||||
<div class="hud-bracket">┘</div>
|
||||
</div>
|
||||
<div class="hud-edge hud-left-edge">
|
||||
<div class="hud-readout"><div id="hud-coll">{{ view.coll }}</div></div>
|
||||
<div class="hud-readout"><div id="hud-ona">{{ view.ona }}</div></div>
|
||||
</div>
|
||||
<div class="hud-edge hud-right-edge">
|
||||
<div v-for="line in view.sensor" :key="line" class="hud-readout"><div>{{ line }}</div></div>
|
||||
</div>
|
||||
<div class="hud-bottom-bar">
|
||||
<span class="hud-readout"><span id="hud-bottom-line">{{ view.bottom }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,6 +8,7 @@ import DisplayControlsView from './DisplayControlsView.vue';
|
||||
import CockpitView from './CockpitView.vue';
|
||||
import SceneChromeView from './SceneChromeView.vue';
|
||||
import ProviderSettingsView from './ProviderSettingsView.vue';
|
||||
import IntelHudView from './IntelHudView.vue';
|
||||
|
||||
/**
|
||||
* Application views by name. A page places each one with a host element,
|
||||
@@ -23,6 +24,7 @@ export const APPLICATION_VIEWS = Object.freeze({
|
||||
'layer-panels': LayerPanelsView,
|
||||
context: ContextView,
|
||||
welcome: WelcomeView,
|
||||
'intel-hud': IntelHudView,
|
||||
});
|
||||
|
||||
const mounted = new WeakMap();
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// src/ui/views/intelHudView.test.mjs — the Intel HUD view renders what
|
||||
// IntelHUD publishes: its readouts, visibility, layout variant and colors.
|
||||
//
|
||||
// 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 hudPageVisibility.test.mjs).
|
||||
import '../../testSupport/registerDom.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createApp, nextTick } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { registerHooks } from 'node:module';
|
||||
import { presentSelectedVessel } from '../../hudReadouts.js';
|
||||
import { useHudStore } from '../stores/hud.js';
|
||||
import IntelHudView from './IntelHudView.vue';
|
||||
|
||||
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');
|
||||
|
||||
const listeners = () => ({ addEventListener() {}, removeEventListener() {} });
|
||||
|
||||
function mount(t) {
|
||||
document.body.replaceChildren();
|
||||
const pinia = createPinia();
|
||||
const store = useHudStore(pinia);
|
||||
const app = createApp(IntelHudView).use(pinia);
|
||||
app.mount(document.body.appendChild(document.createElement('div')));
|
||||
const hud = new IntelHUD(
|
||||
{
|
||||
camera: {
|
||||
pitch: -Math.PI / 2,
|
||||
positionCartographic: { latitude: 0.7, longitude: -1.2, height: 500 },
|
||||
computeViewRectangle: () => undefined,
|
||||
moveStart: listeners(),
|
||||
moveEnd: listeners(),
|
||||
},
|
||||
},
|
||||
{
|
||||
// No AI summary: the readouts are what this test renders.
|
||||
summaryService: { configured: async () => false },
|
||||
publish: (view) => store.publish(view),
|
||||
},
|
||||
);
|
||||
t.after(() => {
|
||||
hud.destroy();
|
||||
presentSelectedVessel(null);
|
||||
app.unmount();
|
||||
});
|
||||
return { hud, root: document.getElementById('intel-hud') };
|
||||
}
|
||||
|
||||
test('the corners are in the document from mount, before IntelHUD presents them', (t) => {
|
||||
document.body.replaceChildren();
|
||||
const pinia = createPinia();
|
||||
const app = createApp(IntelHudView).use(pinia);
|
||||
app.mount(document.body.appendChild(document.createElement('div')));
|
||||
t.after(() => app.unmount());
|
||||
// The panel layout controllers query these synchronously at registration.
|
||||
const root = document.getElementById('intel-hud');
|
||||
assert.equal(root.querySelectorAll('.hud-corner').length, 4);
|
||||
assert.equal(root.querySelectorAll('.hud-edge').length, 2);
|
||||
assert.equal(root.className, '');
|
||||
assert.equal(root.hasAttribute('data-variant'), false);
|
||||
assert.equal(root.getAttribute('style'), null);
|
||||
assert.equal(document.getElementById('hud-summary').textContent, '');
|
||||
assert.equal(root.querySelector('.hud-rec').textContent, '●');
|
||||
});
|
||||
|
||||
test('the first view renders the corners, edges and bottom bar', async (t) => {
|
||||
const { root } = mount(t);
|
||||
await nextTick();
|
||||
assert.equal(root.className, '');
|
||||
assert.equal(root.dataset.variant, 'tactical');
|
||||
assert.deepEqual(
|
||||
[...root.children].map((child) => child.className),
|
||||
[
|
||||
'hud-sonar',
|
||||
'hud-corner hud-top-left',
|
||||
'hud-corner hud-top-right',
|
||||
'hud-corner hud-bottom-left',
|
||||
'hud-corner hud-bottom-right',
|
||||
'hud-edge hud-left-edge',
|
||||
'hud-edge hud-right-edge',
|
||||
'hud-bottom-bar',
|
||||
],
|
||||
);
|
||||
const text = (id) => document.getElementById(id).textContent;
|
||||
assert.equal(text('hud-mode'), 'NORMAL');
|
||||
assert.equal(text('hud-summary'), 'Awaiting telemetry...');
|
||||
assert.equal(text('hud-mgrs'), 'MGRS: ---');
|
||||
assert.equal(text('hud-ais-vessel'), 'AIS: --');
|
||||
assert.equal(
|
||||
root.querySelector('.hud-rec').textContent,
|
||||
'● REC 2026-01-01 00:00:00Z',
|
||||
);
|
||||
assert.equal(
|
||||
document.getElementById('hud-rec-dot').getAttribute('style'),
|
||||
null,
|
||||
);
|
||||
assert.match(
|
||||
root.querySelector('.hud-system').textContent,
|
||||
/^KH11-\d+ {2}OPS-\d+$/,
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...root.querySelectorAll('.hud-right-edge .hud-readout')].map(
|
||||
(line) => line.textContent,
|
||||
),
|
||||
['BAND: PAN', 'BITS: 11', 'LVL: 1A'],
|
||||
);
|
||||
});
|
||||
|
||||
test('visibility, variant, style colors and readouts follow IntelHUD', async (t) => {
|
||||
const { hud, root } = mount(t);
|
||||
hud.onStyleChange('surveillance');
|
||||
hud.setVariant('minimal');
|
||||
hud.setMode('on');
|
||||
await nextTick();
|
||||
assert.equal(root.classList.contains('active'), true);
|
||||
assert.equal(root.dataset.variant, 'minimal');
|
||||
assert.notEqual(root.style.getPropertyValue('--hud-color'), '');
|
||||
assert.equal(document.getElementById('hud-mode').textContent, hud._view.mode);
|
||||
assert.match(document.getElementById('hud-alt').textContent, /^ALT: \d+m/);
|
||||
hud._present({ recShown: false });
|
||||
await nextTick();
|
||||
assert.equal(
|
||||
document.getElementById('hud-rec-dot').style.visibility,
|
||||
'hidden',
|
||||
);
|
||||
hud.setMode('off');
|
||||
await nextTick();
|
||||
assert.equal(root.classList.contains('active'), false);
|
||||
});
|
||||
|
||||
test('the selected vessel readout shows while a vessel is selected', async (t) => {
|
||||
mount(t);
|
||||
presentSelectedVessel({ text: 'AIS: TEST\nCARGO' });
|
||||
await nextTick();
|
||||
const vessel = document.getElementById('hud-ais-vessel');
|
||||
assert.equal(vessel.className, 'hud-ais-vessel active');
|
||||
assert.equal(vessel.textContent, 'AIS: TEST\nCARGO');
|
||||
presentSelectedVessel(null);
|
||||
await nextTick();
|
||||
assert.equal(vessel.className, 'hud-ais-vessel');
|
||||
assert.equal(vessel.textContent, 'AIS: --');
|
||||
});
|
||||
Reference in new issue
Block a user