mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
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:
1 parent
662f51fdf3
commit
b22e138c55
11 files changed
+297
-82
No files matched your search
+2
-1
@@ -211,7 +211,8 @@ out `scene-chrome` the before/after imagery dividers (the imagery still splits,
|
||||
with no line or handle to move it), the status chips, the toast, the ORBIT
|
||||
indicator, the FPS readout, the Scene actions panel, the data-pack attribution
|
||||
cards, the wind inspection marker, the Space Missions replay vehicle, the
|
||||
full-globe celestial ring and the screen-space map annotations;
|
||||
full-globe celestial ring, the screen-space map annotations and the
|
||||
visible map target list;
|
||||
leaving out `command-dock` the voice control and its pointer reticle; and
|
||||
leaving out `context` the Bhote Koshi reconstruction panel. An unknown
|
||||
view name is an error. A host that does not initialize Core's locale passes
|
||||
|
||||
@@ -183,7 +183,13 @@ a feature supplying only its accent.
|
||||
parts as attribute records, the fading marks until they leave; the view
|
||||
hands back the callout text and mark groups the renderer measures, and
|
||||
the tracked-entity fade is measured once the frame is in the document;
|
||||
the styles it injected now live in `src/ui/styles/annotations.css`), then
|
||||
the styles it injected now live in `src/ui/styles/annotations.css`), the
|
||||
visible map target list (the scene chrome renders the buttons and status
|
||||
the world overlay presents through `subscribeWorldOverlayActions()`, which
|
||||
the application shell publishes to the `gev-world-overlay-actions` store
|
||||
and answers with `activateWorldOverlayAction()`; the module needs no Vue
|
||||
for the portable transit entry, and its card and detection canvases stay
|
||||
painting surfaces it draws on, like the Cesium canvas), then
|
||||
Cockpit (in progress: `CockpitBriefing` renders the briefing card's
|
||||
pages, cycle toggle, regional news and local conditions, and
|
||||
`CockpitContext` the Contact card's scope, navigation, counts, nearest
|
||||
|
||||
@@ -1149,5 +1149,7 @@
|
||||
"src/data/bhoteKoshiPanelView.test.mjs",
|
||||
"src/ui/stores/celestialRing.js",
|
||||
"src/celestialRingView.test.mjs",
|
||||
"src/ui/stores/screenAnnotations.js"
|
||||
"src/ui/stores/screenAnnotations.js",
|
||||
"src/ui/stores/worldOverlayActions.js",
|
||||
"src/overlays/worldOverlayActionsView.test.mjs"
|
||||
]
|
||||
@@ -112,7 +112,8 @@
|
||||
"src/ui/stores/celestialRing.js",
|
||||
"src/ui/views/panels/CelestialRing.vue",
|
||||
"src/ui/stores/screenAnnotations.js",
|
||||
"src/ui/views/panels/ScreenAnnotations.vue"
|
||||
"src/ui/views/panels/ScreenAnnotations.vue",
|
||||
"src/ui/stores/worldOverlayActions.js"
|
||||
],
|
||||
"external": ["vue", "pinia"]
|
||||
},
|
||||
@@ -1034,7 +1035,8 @@
|
||||
"src/ui/stores/status.js",
|
||||
"src/ui/toast.js",
|
||||
"src/ui/stores/missionReplay.js",
|
||||
"src/ui/stores/celestialRing.js"
|
||||
"src/ui/stores/celestialRing.js",
|
||||
"src/ui/stores/worldOverlayActions.js"
|
||||
],
|
||||
"external": ["cesium", "vue", "pinia"]
|
||||
},
|
||||
@@ -3033,7 +3035,8 @@
|
||||
"src/ui/stores/missionReplay.js",
|
||||
"src/ui/stores/bhoteKoshiPanel.js",
|
||||
"src/ui/stores/celestialRing.js",
|
||||
"src/ui/stores/screenAnnotations.js"
|
||||
"src/ui/stores/screenAnnotations.js",
|
||||
"src/ui/stores/worldOverlayActions.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
|
||||
@@ -33,9 +33,6 @@ import { t } from '../i18n/index.js';
|
||||
const ROOT_ID = 'world-overlay-root';
|
||||
const CANVAS_ID = 'world-overlay-canvas';
|
||||
const DETECTION_SURFACE_ID = 'world-overlay-detection-surface';
|
||||
const ACCESSIBILITY_ROOT_ID = 'world-overlay-actions';
|
||||
const ACCESSIBILITY_LIST_ID = 'world-overlay-action-list';
|
||||
const ACCESSIBILITY_STATUS_ID = 'world-overlay-status';
|
||||
const PAINT_TARGET_SHARED = 'shared';
|
||||
const PAINT_TARGET_DETECTION = 'detection';
|
||||
const OCCLUDER_PADDING_PX = 6;
|
||||
@@ -156,12 +153,16 @@ let _root = null;
|
||||
let _canvas = null;
|
||||
/** @type {CanvasRenderingContext2D|null} */
|
||||
let _ctx = null;
|
||||
/** @type {HTMLElement|null} Focusable mirror of painted actionable cards. */
|
||||
let _accessibilityRoot = null;
|
||||
/** @type {HTMLElement|null} */
|
||||
let _accessibilityList = null;
|
||||
/** @type {HTMLElement|null} */
|
||||
let _accessibilityStatus = null;
|
||||
/**
|
||||
* The focusable mirror of painted actionable cards, as its view renders it:
|
||||
* `{owner, items, status, announced}`, or null without a host. `announced`
|
||||
* counts accepted focuses, so a repeated announcement is spoken again. Subscribers (the
|
||||
* application shell, which publishes it to a store) receive every change;
|
||||
* the module itself needs no Vue, for the portable transit entry.
|
||||
* @type {?{owner: object, items: object[], status: string, announced: number}}
|
||||
*/
|
||||
let _actions = null;
|
||||
const _actionsListeners = new Set();
|
||||
let _accessibilitySignature = '';
|
||||
const _accessibleActivatorByKey = new Map();
|
||||
/** @type {HTMLCanvasElement|null} Host-owned detection blend-isolation surface. */
|
||||
@@ -1259,30 +1260,10 @@ function ensureOverlayDom() {
|
||||
_root.setAttribute('aria-hidden', 'true');
|
||||
_canvas.setAttribute('aria-hidden', 'true');
|
||||
_detectionSurface.setAttribute('aria-hidden', 'true');
|
||||
_accessibilityRoot = document.getElementById(ACCESSIBILITY_ROOT_ID);
|
||||
if (!_accessibilityRoot) {
|
||||
_accessibilityRoot = document.createElement('div');
|
||||
_accessibilityRoot.id = ACCESSIBILITY_ROOT_ID;
|
||||
_accessibilityRoot.className = 'world-overlay-accessibility';
|
||||
_accessibilityRoot.setAttribute('role', 'region');
|
||||
_accessibilityRoot.setAttribute('aria-label', t('Visible map targets'));
|
||||
document.body.appendChild(_accessibilityRoot);
|
||||
}
|
||||
_accessibilityList = document.getElementById(ACCESSIBILITY_LIST_ID);
|
||||
if (!_accessibilityList) {
|
||||
_accessibilityList = document.createElement('div');
|
||||
_accessibilityList.id = ACCESSIBILITY_LIST_ID;
|
||||
_accessibilityRoot.appendChild(_accessibilityList);
|
||||
}
|
||||
_accessibilityStatus = document.getElementById(ACCESSIBILITY_STATUS_ID);
|
||||
if (!_accessibilityStatus) {
|
||||
_accessibilityStatus = document.createElement('div');
|
||||
_accessibilityStatus.id = ACCESSIBILITY_STATUS_ID;
|
||||
_accessibilityStatus.setAttribute('role', 'status');
|
||||
_accessibilityStatus.setAttribute('aria-live', 'polite');
|
||||
_accessibilityStatus.setAttribute('aria-atomic', 'true');
|
||||
_accessibilityRoot.appendChild(_accessibilityStatus);
|
||||
}
|
||||
// Each host owns its action list; a click from an earlier host's list is
|
||||
// ignored.
|
||||
if (!_actions)
|
||||
presentActions({ owner: {}, items: [], status: '', announced: 0 });
|
||||
_ctx = _canvas.getContext('2d', { alpha: true, desynchronized: true });
|
||||
_detectionCtx = _detectionSurface.getContext('2d', {
|
||||
alpha: true,
|
||||
@@ -2371,9 +2352,46 @@ function publishPaintRect(item) {
|
||||
if (record.entry.interactive) _hitRects[_hitRectCount++] = rect;
|
||||
}
|
||||
|
||||
/** Publish the action list to every subscriber. */
|
||||
function presentActions(next) {
|
||||
_actions = next && Object.freeze(next);
|
||||
for (const listener of _actionsListeners) listener(_actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive the visible map target list now and after every change.
|
||||
* @param {(view: ?object) => void} listener
|
||||
* @returns {() => void} Unsubscribe.
|
||||
*/
|
||||
export function subscribeWorldOverlayActions(listener) {
|
||||
_actionsListeners.add(listener);
|
||||
listener(_actions);
|
||||
return () => _actionsListeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus a listed target, as its button does, announcing an accepted focus.
|
||||
* @param {object} owner The `owner` of the list the button belongs to.
|
||||
* @param {string} key The item's key.
|
||||
*/
|
||||
export function activateWorldOverlayAction(owner, key) {
|
||||
if (!_actions || owner !== _actions.owner) return;
|
||||
const item = _actions.items.find((candidate) => candidate.key === key);
|
||||
const activate = _accessibleActivatorByKey.get(key);
|
||||
if (!item || !activate) return;
|
||||
const accepted = activate();
|
||||
if (accepted !== false && _actions?.owner === owner) {
|
||||
presentActions({
|
||||
..._actions,
|
||||
status: t('Focusing {label}', { label: item.label }),
|
||||
announced: _actions.announced + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Keep a stable, bounded accessible mirror of currently painted actions. */
|
||||
function syncAccessibleActions() {
|
||||
if (!_accessibilityList) return;
|
||||
if (!_actions) return;
|
||||
_accessibleActivatorByKey.clear();
|
||||
const items = [];
|
||||
for (let i = _hitRectCount - 1; i >= 0; i--) {
|
||||
@@ -2397,31 +2415,7 @@ function syncAccessibleActions() {
|
||||
.join('\u0001');
|
||||
if (signature === _accessibilitySignature) return;
|
||||
_accessibilitySignature = signature;
|
||||
if (typeof _accessibilityList.replaceChildren === 'function') {
|
||||
_accessibilityList.replaceChildren();
|
||||
} else {
|
||||
while (_accessibilityList.firstChild)
|
||||
_accessibilityList.removeChild(_accessibilityList.firstChild);
|
||||
}
|
||||
for (const item of items) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = item.label;
|
||||
button.setAttribute('aria-label', item.label);
|
||||
button.setAttribute('aria-pressed', String(item.selected));
|
||||
button.dataset.overlayActionKey = item.key;
|
||||
button.addEventListener?.('click', () => {
|
||||
const activate = _accessibleActivatorByKey.get(item.key);
|
||||
if (!activate) return;
|
||||
const accepted = activate();
|
||||
if (accepted !== false && _accessibilityStatus) {
|
||||
_accessibilityStatus.textContent = t('Focusing {label}', {
|
||||
label: item.label,
|
||||
});
|
||||
}
|
||||
});
|
||||
_accessibilityList.appendChild(button);
|
||||
}
|
||||
presentActions({ ..._actions, items });
|
||||
}
|
||||
|
||||
let _detectionSurfacePrepared = false;
|
||||
@@ -2739,7 +2733,6 @@ export function destroyWorldOverlay() {
|
||||
if (_occluderRefreshTimer != null) clearTimeout(_occluderRefreshTimer);
|
||||
_occluderRefreshTimer = null;
|
||||
_root?.remove?.();
|
||||
_accessibilityRoot?.remove?.();
|
||||
// The detection surface is not a child of `_root` (it lives in the Cesium
|
||||
// container so its `screen` blend reaches the scene), so it has to be torn
|
||||
// down explicitly rather than by the root's removal.
|
||||
@@ -2787,9 +2780,7 @@ export function destroyWorldOverlay() {
|
||||
_root = null;
|
||||
_canvas = null;
|
||||
_ctx = null;
|
||||
_accessibilityRoot = null;
|
||||
_accessibilityList = null;
|
||||
_accessibilityStatus = null;
|
||||
if (_actions) presentActions(null);
|
||||
_detectionSurface = null;
|
||||
_detectionCtx = null;
|
||||
_occluder = null;
|
||||
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
getOverlayPaintRect,
|
||||
getWorldOverlayDiagnostics,
|
||||
hitTestWorldOverlay,
|
||||
activateWorldOverlayAction,
|
||||
initWorldOverlay,
|
||||
subscribeWorldOverlayActions,
|
||||
isOverlayPointVisible,
|
||||
normalizeOverlayEntry,
|
||||
paintLaneForOverlayEntry,
|
||||
@@ -1019,7 +1021,14 @@ test('accessible actions announce only accepted focus and expose selected state'
|
||||
const env = installMockEnvironment();
|
||||
let acceptedActivations = 0;
|
||||
let staleActivations = 0;
|
||||
let actions = null;
|
||||
const unsubscribe = subscribeWorldOverlayActions((view) => {
|
||||
actions = view;
|
||||
});
|
||||
initWorldOverlay(env.viewer);
|
||||
assert.deepEqual(actions.items, []);
|
||||
assert.equal(actions.status, '');
|
||||
assert.equal(actions.announced, 0);
|
||||
setOverlayEntries('accessible-actions', [selectedEntry('vessel:123', {
|
||||
interactive: true,
|
||||
accessibilityLabel: 'Focus vessel TEST, MMSI 123',
|
||||
@@ -1030,15 +1039,20 @@ test('accessible actions announce only accepted focus and expose selected state'
|
||||
})]);
|
||||
env.postRender.raise();
|
||||
|
||||
const list = env.document.getElementById('world-overlay-action-list');
|
||||
const status = env.document.getElementById('world-overlay-status');
|
||||
assert.equal(list.children.length, 1);
|
||||
assert.equal(list.children[0]['aria-pressed'], 'true');
|
||||
list.children[0].click();
|
||||
assert.equal(actions.items.length, 1);
|
||||
const [item] = actions.items;
|
||||
assert.equal(item.label, 'Focus vessel TEST, MMSI 123');
|
||||
assert.equal(item.selected, true);
|
||||
const owner = actions.owner;
|
||||
activateWorldOverlayAction({}, item.key);
|
||||
assert.equal(acceptedActivations, 0, 'a list from another host cannot activate');
|
||||
activateWorldOverlayAction(owner, item.key);
|
||||
assert.equal(acceptedActivations, 1);
|
||||
assert.equal(status.textContent, 'Focusing Focus vessel TEST, MMSI 123');
|
||||
assert.equal(actions.status, 'Focusing Focus vessel TEST, MMSI 123');
|
||||
assert.equal(actions.announced, 1);
|
||||
activateWorldOverlayAction(owner, item.key);
|
||||
assert.equal(actions.announced, 2, 'a repeated focus is a new announcement');
|
||||
|
||||
status.textContent = 'Focus unchanged';
|
||||
setOverlayEntries('accessible-actions', [selectedEntry('vessel:123', {
|
||||
selected: false,
|
||||
interactive: true,
|
||||
@@ -1049,12 +1063,16 @@ test('accessible actions announce only accepted focus and expose selected state'
|
||||
},
|
||||
})]);
|
||||
env.postRender.raise();
|
||||
assert.equal(list.children.length, 1);
|
||||
assert.equal(list.children[0]['aria-pressed'], 'false');
|
||||
list.children[0].click();
|
||||
assert.equal(actions.items.length, 1);
|
||||
assert.equal(actions.items[0].selected, false);
|
||||
assert.equal(actions.status, 'Focusing Focus vessel TEST, MMSI 123', 'the list keeps its last announcement');
|
||||
activateWorldOverlayAction(owner, actions.items[0].key);
|
||||
assert.equal(staleActivations, 1);
|
||||
assert.equal(status.textContent, 'Focus unchanged');
|
||||
assert.equal(actions.status, 'Focusing Focus vessel TEST, MMSI 123', 'a refused focus announces nothing');
|
||||
assert.equal(actions.announced, 2);
|
||||
env.cleanup();
|
||||
assert.equal(actions, null, 'teardown withdraws the list');
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
test('viewport and horizon culling obey explicit policy inputs', () => {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// src/overlays/worldOverlayActionsView.test.mjs — the scene chrome's visible
|
||||
// map target list renders what the world overlay presents and activates the
|
||||
// target of the list each button belongs to.
|
||||
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 { useWorldOverlayActionsStore } from '../ui/stores/worldOverlayActions.js';
|
||||
import SceneChromeView from '../ui/views/SceneChromeView.vue';
|
||||
|
||||
function mount(t) {
|
||||
document.body.replaceChildren();
|
||||
const pinia = createPinia();
|
||||
const store = useWorldOverlayActionsStore(pinia);
|
||||
const app = createApp(SceneChromeView).use(pinia);
|
||||
app.mount(document.body.appendChild(document.createElement('div')));
|
||||
t.after(() => app.unmount());
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
return { store, $ };
|
||||
}
|
||||
|
||||
test('the target list renders each painted action and the last announcement', async (t) => {
|
||||
const { store, $ } = mount(t);
|
||||
const region = $('#world-overlay-actions');
|
||||
assert.equal(region.getAttribute('role'), 'region');
|
||||
assert.equal(region.className, 'world-overlay-accessibility');
|
||||
assert.equal($('#world-overlay-action-list').children.length, 0);
|
||||
assert.equal($('#world-overlay-status').textContent, '');
|
||||
const owner = {};
|
||||
store.publish({
|
||||
owner,
|
||||
items: [
|
||||
{ key: 'vessel\u0000123', label: 'Focus vessel TEST', selected: true },
|
||||
{ key: 'fire\u000042', label: 'Focus fire 42', selected: false },
|
||||
],
|
||||
status: 'Focusing Focus vessel TEST',
|
||||
announced: 1,
|
||||
});
|
||||
await nextTick();
|
||||
const buttons = [...$('#world-overlay-action-list').children];
|
||||
assert.deepEqual(
|
||||
buttons.map((button) => [
|
||||
button.type,
|
||||
button.textContent,
|
||||
button.getAttribute('aria-label'),
|
||||
button.getAttribute('aria-pressed'),
|
||||
button.dataset.overlayActionKey,
|
||||
]),
|
||||
[
|
||||
[
|
||||
'button',
|
||||
'Focus vessel TEST',
|
||||
'Focus vessel TEST',
|
||||
'true',
|
||||
'vessel\u0000123',
|
||||
],
|
||||
['button', 'Focus fire 42', 'Focus fire 42', 'false', 'fire\u000042'],
|
||||
],
|
||||
);
|
||||
assert.equal(
|
||||
$('#world-overlay-status').textContent,
|
||||
'Focusing Focus vessel TEST',
|
||||
);
|
||||
});
|
||||
|
||||
test('a button activates its own list’s target', async (t) => {
|
||||
const { store, $ } = mount(t);
|
||||
const calls = [];
|
||||
store.connect({ activate: (owner, key) => calls.push([owner, key]) });
|
||||
const owner = {};
|
||||
store.publish({
|
||||
owner,
|
||||
items: [{ key: 'a', label: 'Focus A', selected: false }],
|
||||
status: '',
|
||||
});
|
||||
await nextTick();
|
||||
$('#world-overlay-action-list button').click();
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0][0] === owner);
|
||||
assert.equal(calls[0][1], 'a');
|
||||
store.publish(null);
|
||||
await nextTick();
|
||||
assert.equal($('#world-overlay-action-list').children.length, 0);
|
||||
assert.ok($('#world-overlay-actions'), 'the region itself stays');
|
||||
});
|
||||
|
||||
test('a repeated focus rewrites the status so it is announced again', async (t) => {
|
||||
const { store, $ } = mount(t);
|
||||
const owner = {};
|
||||
const list = (announced) => ({
|
||||
owner,
|
||||
items: [{ key: 'a', label: 'Focus A', selected: false }],
|
||||
status: 'Focusing Focus A',
|
||||
announced,
|
||||
});
|
||||
store.publish(list(1));
|
||||
await nextTick();
|
||||
const status = $('#world-overlay-status');
|
||||
assert.equal(status.textContent, 'Focusing Focus A');
|
||||
let writes = 0;
|
||||
new MutationObserver((records) => (writes += records.length)).observe(
|
||||
status,
|
||||
{
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
},
|
||||
);
|
||||
store.publish({
|
||||
...list(1),
|
||||
items: [{ key: 'a', label: 'Focus A', selected: true }],
|
||||
});
|
||||
await nextTick();
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
assert.equal(writes, 0, 'a list change alone announces nothing');
|
||||
store.publish(list(2));
|
||||
await nextTick();
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
assert.ok(writes > 0, 'the second focus of the same target is written again');
|
||||
assert.equal(status.textContent, 'Focusing Focus A');
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
);
|
||||
@@ -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 -->
|
||||
|
||||
Reference in new issue
Block a user