feat(awareness): render the Global Context compass from a store

The awareness layer presents the compass ring, heading, cardinal labels and
contact bearing markers through subscribeDirectionOverlay(); the application
shell publishes them to the gev-awareness-direction store and the scene
chrome renders them in the viewer's container.
This commit is contained in:
Sameh Khamis committed 2026-10-31 22:01:56 -07:00
1 parent b22e138c55
commit b96cc37ab5
14 files changed
+389 -100

No files matched your search

+13
View File
@@ -57,6 +57,7 @@ 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 { useAwarenessDirectionStore } from './stores/awarenessDirection.js';
import { useDisplayStore } from './stores/display.js';
import { useLocaleStore } from './stores/locale.js';
import { useLocationStore } from './stores/location.js';
@@ -1042,6 +1043,15 @@ export class StyleManager extends ShellFacade {
this._awarenessPanelUnsubscribe = militaryAwarenessLayer?.subscribePanel?.(
(view) => contextStore.publishAwareness(view),
);
// And its compass and contact bearings, which the scene chrome renders.
const awarenessDirectionStore =
useAwarenessDirectionStore(applicationPinia());
this._awarenessDirectionUnsubscribe?.();
awarenessDirectionStore.publish(null);
this._awarenessDirectionUnsubscribe =
militaryAwarenessLayer?.subscribeDirectionOverlay?.((view) =>
awarenessDirectionStore.publish(view),
);
// The Space Missions layer publishes its roster and selected mission.
contextStore.connectMissions(rocketLaunchesLayer);
this._missionsPanelUnsubscribe?.();
@@ -1805,6 +1815,9 @@ export class StyleManager extends ShellFacade {
this._localSdrControls?.destroy();
this._awarenessPanelUnsubscribe?.();
this._awarenessPanelUnsubscribe = null;
this._awarenessDirectionUnsubscribe?.();
this._awarenessDirectionUnsubscribe = null;
useAwarenessDirectionStore(applicationPinia()).publish(null);
this._missionsPanelUnsubscribe?.();
this._missionsPanelUnsubscribe = null;
this._missionReplayUnsubscribe?.();
+28
View File
@@ -0,0 +1,28 @@
import { defineStore } from 'pinia';
import { shallowRef } from 'vue';
/**
* The Global Context compass and contact direction overlay as its view
* renders it. The awareness layer presents it through
* `subscribeDirectionOverlay()` and the application shell publishes it here.
*/
export const useAwarenessDirectionStore = defineStore(
'gev-awareness-direction',
() => {
/**
* `{container, hidden, ring, heading, labels, markers}`, or null while
* the layer has no overlay. `container` is the element it sits in.
*/
const overlay = shallowRef(null);
/**
* @param {?object} next A fresh presentation, frozen by the layer; every
* publish re-renders.
*/
function publish(next) {
overlay.value = next;
}
return { overlay, publish };
},
);
+2
View File
@@ -14,6 +14,7 @@ import SplitFlapText from './panels/SplitFlapText.vue';
import WindInspectionMarker from './panels/WindInspectionMarker.vue';
import CelestialRing from './panels/CelestialRing.vue';
import ScreenAnnotations from './panels/ScreenAnnotations.vue';
import AwarenessDirection from './panels/AwarenessDirection.vue';
// The ACTIVE STYLE corner shows the style VisualSettings publishes.
const { style } = storeToRefs(useDisplayStore());
@@ -127,4 +128,5 @@ const vToastText = {
<WindInspectionMarker />
<MissionReplayOverlay />
<ScreenAnnotations />
<AwarenessDirection />
</template>
@@ -0,0 +1,18 @@
<script setup>
import { storeToRefs } from 'pinia';
import { useAwarenessDirectionStore } from '../../stores/awarenessDirection.js';
// The Global Context compass and the nearest contacts' bearings, inside the
// viewer's container, as the awareness layer presents them.
const { overlay } = storeToRefs(useAwarenessDirectionStore());
// A part keeps no inline placement until the layer first places it.
const place = (part) =>
part.left === null ? undefined : { left: part.left, top: part.top };
</script>
<template>
<Teleport v-if="overlay" :to="overlay.container">
<div id="military-awareness-direction-overlay" :hidden="overlay.hidden" aria-hidden="true"><span v-for="label in overlay.labels" :key="label.label" :class="`military-awareness-compass-label cardinal-${label.label.toLowerCase()}`" :data-bearing="String(label.bearing)" :style="place(label)">{{ label.label }}</span><div class="military-awareness-compass-ring" :style="overlay.ring ?? undefined"></div><span class="military-awareness-compass-heading" :style="place(overlay.heading)">{{ overlay.heading.text }}</span><div v-for="(marker, index) in overlay.markers" :key="index" class="military-awareness-direction-marker" :hidden="marker.hidden" :style="marker.style ?? undefined"><span class="military-awareness-direction-arrow" :style="marker.arrow ?? undefined">➜</span><span class="military-awareness-direction-label" :style="marker.caption.style ?? undefined">{{ marker.caption.text }}</span></div></div>
</Teleport>
</template>