feat(launches): render the replay vehicle overlay from a store

The launches layer keeps the replay state, projection and wording and
presents the replay vehicle overlay through subscribeReplayOverlay(),
sending nothing for an unchanged frame, so the portable launches entry
stays free of Vue. The application shell publishes each presentation to
the gev-mission-replay store and the scene chrome teleports the vehicle,
orbit dot and callout into the globe container with the same markup,
classes, mode, transform, rotation and sonar opacities. Hiding keeps the
mode, place and text, as before.
This commit is contained in:
Sameh Khamis committed 2026-10-31 20:53:54 -07:00
1 parent e0bfa471ac
commit 9ea6d7e8aa
12 files changed
+253 -84

No files matched your search

+12
View File
@@ -54,6 +54,7 @@ import { applyLocaleChoice } from './localePicker.js';
import { PanelNavigation } from './panelNavigation.js';
import { applicationPinia, createUiRuntime } from './vueRuntime.js';
import { useContextStore } from './stores/context.js';
import { useMissionReplayStore } from './stores/missionReplay.js';
import { useDisplayStore } from './stores/display.js';
import { useLocaleStore } from './stores/locale.js';
import { useLocationStore } from './stores/location.js';
@@ -1036,6 +1037,14 @@ export class StyleManager extends ShellFacade {
this._missionsPanelUnsubscribe = rocketLaunchesLayer?.subscribePanel?.(
(view) => contextStore.publishMissions(view),
);
// And its replay vehicle overlay, which the scene chrome renders.
const missionReplayStore = useMissionReplayStore(applicationPinia());
this._missionReplayUnsubscribe?.();
missionReplayStore.publish(null);
this._missionReplayUnsubscribe =
rocketLaunchesLayer?.subscribeReplayOverlay?.((view) =>
missionReplayStore.publish(view),
);
}
/** Wire the independent Radio companion controls. */
@@ -1786,6 +1795,9 @@ export class StyleManager extends ShellFacade {
this._awarenessPanelUnsubscribe = null;
this._missionsPanelUnsubscribe?.();
this._missionsPanelUnsubscribe = null;
this._missionReplayUnsubscribe?.();
this._missionReplayUnsubscribe = null;
useMissionReplayStore(applicationPinia()).publish(null);
const contextStore = useContextStore(applicationPinia());
contextStore.connectAwareness(null);
contextStore.publishAwareness(null);
+22
View File
@@ -0,0 +1,22 @@
import { defineStore } from 'pinia';
import { markRaw, shallowRef } from 'vue';
/**
* The Space Missions replay vehicle overlay as its view renders it. The
* launches layer presents it through `subscribeReplayOverlay()` and the
* application shell publishes each presentation here.
*/
export const useMissionReplayStore = defineStore('gev-mission-replay', () => {
/**
* `{visible, mode, thrusting, paused, transform, rotation, iconOpacity,
* labelOpacity, title, detail}`, or null while the layer has no overlay.
*/
const overlay = shallowRef(null);
/** @param {?object} next A fresh presentation; every publish re-renders. */
function publish(next) {
overlay.value = next && markRaw(next);
}
return { overlay, publish };
});
+2
View File
@@ -6,6 +6,7 @@ import { useLocationStore } from '../stores/location.js';
import { useStatusStore } from '../stores/status.js';
import { vClassToggle } from '../rail/classToggle.js';
import ImagerySplitView from './panels/ImagerySplitView.vue';
import MissionReplayOverlay from './panels/MissionReplayOverlay.vue';
import DataPacks from './panels/DataPacks.vue';
import SceneInteractions from './panels/SceneInteractions.vue';
import SplitFlapText from './panels/SplitFlapText.vue';
@@ -102,4 +103,5 @@ const vToastText = {
<DataPacks />
<SceneInteractions />
<WindInspectionMarker />
<MissionReplayOverlay />
</template>
@@ -0,0 +1,14 @@
<script setup>
import { storeToRefs } from 'pinia';
import { useMissionReplayStore } from '../../stores/missionReplay.js';
// The replay vehicle, its orbit dot and callout over the globe, where the
// launches layer places them after each rendered frame.
const { overlay } = storeToRefs(useMissionReplayStore());
</script>
<template>
<Teleport v-if="overlay" defer to="#cesiumContainer">
<div class="mission-replay-vehicle-overlay" :class="{ 'is-thrusting': overlay.thrusting, 'is-paused': overlay.paused }" :hidden="!overlay.visible" aria-hidden="true" :data-mode="overlay.mode ?? undefined" :style="{ transform: overlay.transform ?? undefined, '--mission-replay-rotation': overlay.rotation ?? undefined }"><div class="mission-replay-flight-symbol" :style="{ opacity: overlay.iconOpacity ?? undefined }"><svg class="mission-replay-rocket" viewBox="0 0 48 72" aria-hidden="true"><path class="mission-replay-rocket-body" d="M24 5C16 14 14 27 15 45L9 54L18 51L24 58L30 51L39 54L33 45C34 27 32 14 24 5Z"></path><circle class="mission-replay-rocket-port" cx="24" cy="29" r="3.4"></circle></svg><svg class="mission-replay-thrust" viewBox="0 0 72 72" aria-hidden="true"><ellipse style="--thrust-index:0" cx="36" cy="7" rx="5" ry="1.8"></ellipse><ellipse style="--thrust-index:1" cx="36" cy="15" rx="8" ry="2.4"></ellipse><ellipse style="--thrust-index:2" cx="36" cy="24" rx="11" ry="3"></ellipse><ellipse style="--thrust-index:3" cx="36" cy="35" rx="15" ry="3.8"></ellipse><ellipse style="--thrust-index:4" cx="36" cy="48" rx="20" ry="4.7"></ellipse><ellipse style="--thrust-index:5" cx="36" cy="63" rx="26" ry="5.8"></ellipse></svg></div><div class="mission-replay-orbit-dot" aria-hidden="true" :style="{ opacity: overlay.iconOpacity ?? undefined }"></div><div class="mission-replay-overlay-callout" :style="{ opacity: overlay.labelOpacity ?? undefined }"><strong data-replay-overlay-title="">{{ overlay.title }}</strong><span data-replay-overlay-detail="">{{ overlay.detail }}</span></div></div>
</Teleport>
</template>