feat(world-overlay): render the visible map target list from a store

The world overlay presents its focusable mirror of painted cards through
subscribeWorldOverlayActions() and answers activateWorldOverlayAction();
the application shell publishes the list to a store and the scene chrome
renders the buttons and the status, announcing a repeated focus again.
The module stays free of Vue for the portable transit entry, and its
canvases remain painting surfaces.
This commit is contained in:
Sameh Khamis committed 2026-10-31 21:52:32 -07:00
1 parent 662f51fdf3
commit b22e138c55
11 files changed
+297 -82

No files matched your search

+15
View File
@@ -56,6 +56,7 @@ import { applicationPinia, createUiRuntime } from './vueRuntime.js';
import { useContextStore } from './stores/context.js';
import { useMissionReplayStore } from './stores/missionReplay.js';
import { useCelestialRingStore } from './stores/celestialRing.js';
import { useWorldOverlayActionsStore } from './stores/worldOverlayActions.js';
import { useDisplayStore } from './stores/display.js';
import { useLocaleStore } from './stores/locale.js';
import { useLocationStore } from './stores/location.js';
@@ -106,6 +107,8 @@ export class StyleManager extends ShellFacade {
CelestialRing,
initTrackedReadout,
initWorldOverlay,
subscribeWorldOverlayActions,
activateWorldOverlayAction,
initDetection,
setDetectionStyle,
trafficLayer,
@@ -479,6 +482,13 @@ export class StyleManager extends ShellFacade {
// detection and tracked-readout initialize. It stays transparent until a
// production source explicitly registers entries.
initWorldOverlay(viewer);
// Its focusable mirror of the painted cards, which the scene chrome
// renders.
const overlayActionsStore = useWorldOverlayActionsStore(applicationPinia());
overlayActionsStore.connect({ activate: activateWorldOverlayAction });
this._overlayActionsUnsubscribe = subscribeWorldOverlayActions((view) =>
overlayActionsStore.publish(view),
);
// Initialize detection overlay BEFORE style stages so the composite
// stage is first in the post-process pipeline
@@ -1834,7 +1844,12 @@ export class StyleManager extends ShellFacade {
}
destroyTrackedReadout();
destroyDetection();
this._overlayActionsUnsubscribe?.();
this._overlayActionsUnsubscribe = null;
destroyWorldOverlay();
const overlayActionsStore = useWorldOverlayActionsStore(applicationPinia());
overlayActionsStore.connect(null);
overlayActionsStore.publish(null);
this.celestialRing?.destroy();
this._visualSettings.destroy();
}
+4
View File
@@ -27,8 +27,10 @@ import {
initTrackedReadout,
} from '../data/trackedReadout.js';
import {
activateWorldOverlayAction,
destroyWorldOverlay,
initWorldOverlay,
subscribeWorldOverlayActions,
} from '../overlays/worldOverlay.js';
import {
destroyDetection,
@@ -88,6 +90,8 @@ export class StyleManager extends ApplicationShell {
initTrackedReadout,
destroyWorldOverlay,
initWorldOverlay,
subscribeWorldOverlayActions,
activateWorldOverlayAction,
destroyDetection,
initDetection,
cycleDetectionMode,
+34
View File
@@ -0,0 +1,34 @@
import { defineStore } from 'pinia';
import { markRaw, shallowRef } from 'vue';
/**
* The visible map target list as its view renders it. The world overlay
* (src/overlays/worldOverlay.js) presents the actionable cards it painted and
* the last focus announcement; the application shell publishes them here and
* connects the overlay's activation.
*/
export const useWorldOverlayActionsStore = defineStore(
'gev-world-overlay-actions',
() => {
/**
* `{owner, items: [{key, label, selected}], status}`, or null without an
* overlay host. `owner` is passed back with each activation.
*/
const actions = shallowRef(null);
/** @param {?object} next A fresh presentation; every publish re-renders. */
function publish(next) {
actions.value = next && markRaw(next);
}
/** The controls whose `activate(owner, key)` a target button calls. */
const controls = shallowRef(null);
/** @param {?object} next The controls, or null to disconnect. */
function connect(next) {
controls.value = next && markRaw(next);
}
return { actions, publish, controls, connect };
},
);
+21 -2
View File
@@ -4,6 +4,7 @@ import { t } from '../../i18n/index.js';
import { useDisplayStore } from '../stores/display.js';
import { useLocationStore } from '../stores/location.js';
import { useStatusStore } from '../stores/status.js';
import { useWorldOverlayActionsStore } from '../stores/worldOverlayActions.js';
import { vClassToggle } from '../rail/classToggle.js';
import ImagerySplitView from './panels/ImagerySplitView.vue';
import MissionReplayOverlay from './panels/MissionReplayOverlay.vue';
@@ -21,6 +22,24 @@ const { style } = storeToRefs(useDisplayStore());
const { loading, traffic, cctv, toast, fps } = storeToRefs(useStatusStore());
// The ORBIT indicator LocationControls offers with the location tray.
const { view: location } = storeToRefs(useLocationStore());
// The focusable mirror of the overlay cards the world overlay painted.
const { actions: targets, controls: overlayControls } = storeToRefs(
useWorldOverlayActionsStore(),
);
// Each button activates the target of the list it was rendered for.
const onOverlayAction = (owner, key) => () =>
overlayControls.value?.activate(owner, key);
// Every accepted focus rewrites the status, so the live region announces a
// repeated focus again.
const vOverlayStatus = {
mounted(el, { value }) {
el.textContent = value?.status ?? '';
},
updated(el, { value, oldValue }) {
if (value?.announced !== oldValue?.announced || !value)
el.textContent = value?.status ?? '';
},
};
// Every show rewrites the toast's text, so the live region announces a
// repeated message again; hiding leaves it as it fades.
const vToastText = {
@@ -39,8 +58,8 @@ const vToastText = {
<canvas id="world-overlay-canvas"></canvas>
</div>
<div id="world-overlay-actions" class="world-overlay-accessibility" role="region" aria-label="Visible map targets" data-i18n-aria-label>
<div id="world-overlay-action-list"></div>
<div id="world-overlay-status" role="status" aria-live="polite" aria-atomic="true"></div>
<div id="world-overlay-action-list"><button v-for="item in targets?.items ?? []" :key="item.key" type="button" :aria-label="item.label" :aria-pressed="String(item.selected)" :data-overlay-action-key="item.key" :onClick="onOverlayAction(targets.owner, item.key)">{{ item.label }}</button></div>
<div id="world-overlay-status" v-overlay-status="targets" role="status" aria-live="polite" aria-atomic="true"></div>
</div>
<!-- Title -->