From 3a750c9021a8d021b70fde7b5f6c99758d3749a2 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 14:15:36 -0800 Subject: [PATCH] feat(recording): render the safe frame from a recording store RecordingControls publishes the safe frame a recording composes in, 16:9 or 9:16 (null while none records), to the new recording store, and the scene chrome renders the overlay's active and ratio classes from it instead of the controls writing them. Any frame but 9:16, even none named, records in 16:9 as before. --- CHANGELOG.md | 4 ++- docs/VUE-MIGRATION.md | 4 ++- scripts/format-scope.json | 1 + scripts/package-boundaries.json | 9 +++-- src/ui/applicationShell.js | 3 ++ src/ui/recordingControls.js | 38 +++++++------------- src/ui/safeFrameView.test.mjs | 59 ++++++++++++++++++++++++++++++++ src/ui/shellElements.js | 2 -- src/ui/shellLifecycle.test.mjs | 11 +++--- src/ui/stores/recording.js | 18 ++++++++++ src/ui/views/SceneChromeView.vue | 5 ++- 11 files changed, 116 insertions(+), 38 deletions(-) create mode 100644 src/ui/safeFrameView.test.mjs create mode 100644 src/ui/stores/recording.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 17abe6f3f..56a8fb537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,7 +74,9 @@ `UiEmpty`. The top-center clear, share, tilt, north-up and reset buttons, and the cockpit's reset, call the shell through the new globe actions store, - which also carries the camera's tilt and compass heading. `syncScopeControls` (`./ui/display`), + which also carries the camera's tilt and compass heading. The recording + safe frame renders what `RecordingControls` publishes to the new + recording store. `syncScopeControls` (`./ui/display`), `shouldHideCollapsedRightPanels` (`./ui/layout`) and `bindClearLayersControl` (`./ui/layers`) are deprecated: the Display view, the rail and the globe actions view now own what they did. `./ui/input` imports Vue diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index e3d04b7b7..92bde9fa5 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -233,7 +233,9 @@ a feature supplying only its accent. that `bindCameraOrientationControls` publishes each drawn frame it changes, which only `GlobeCompass` renders; an owner publishes only while it answers the button, so a shell replaced by a newer one leaves - its state alone), the Scene actions panel (`SceneInteractions` + its state alone), the recording safe frame (the scene chrome renders + the frame `RecordingControls` publishes to the `gev-recording` store), + the Scene actions panel (`SceneInteractions` teleports what the scene interactions owner publishes to the `gev-scene-interactions` store into the viewer's container; the owner keeps the pick handler and focuses the picked action), the data-pack diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 35d4e7df4..c86531fc3 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1135,6 +1135,7 @@ "src/ui/kit/UiChip.vue", "src/ui/kit/UiChip.test.mjs", "src/ui/globeActions.test.mjs", + "src/ui/safeFrameView.test.mjs", "src/ui/kit/UiSelect.js", "src/ui/kit/UiSelect.test.mjs", "src/ui/kit/UiBadge.vue", diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 876228af9..f2992d749 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -141,7 +141,8 @@ "src/ui/kit/UiBadge.vue", "src/ui/stores/globeActions.js", "src/ui/views/panels/GlobeActions.vue", - "src/ui/views/panels/GlobeCompass.vue" + "src/ui/views/panels/GlobeCompass.vue", + "src/ui/stores/recording.js" ], "external": ["vue", "pinia"] }, @@ -1080,7 +1081,8 @@ "src/ui/kit/UiSlider.js", "src/ui/kit/UiButton.vue", "src/ui/kit/UiBadge.vue", - "src/ui/stores/globeActions.js" + "src/ui/stores/globeActions.js", + "src/ui/stores/recording.js" ], "external": ["cesium", "vue", "pinia"] }, @@ -3094,7 +3096,8 @@ "src/ui/kit/UiButton.vue", "src/ui/kit/UiEmpty.vue", "src/ui/kit/UiBadge.vue", - "src/ui/stores/globeActions.js" + "src/ui/stores/globeActions.js", + "src/ui/stores/recording.js" ], "external": [ "hls.js", diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index 1fc2c1f05..9df0b1f17 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -63,6 +63,7 @@ import { useLocaleStore } from './stores/locale.js'; import { useLocationStore } from './stores/location.js'; import { useCockpitStore } from './stores/cockpit.js'; import { useGlobeActionsStore } from './stores/globeActions.js'; +import { useRecordingStore } from './stores/recording.js'; /** * Central UI orchestrator for the God's Eye View application. @@ -129,6 +130,8 @@ export class StyleManager extends ShellFacade { this._recording = new RecordingControls({ syncShareState: () => this._syncShareState(), syncHudControls: () => this._visualSettings._syncHudControls(), + publishSafeFrame: (frame) => + useRecordingStore(applicationPinia()).publishSafeFrame(frame), }); Object.assign(this, readShellElements()); this._panelChrome = new PanelChrome({ diff --git a/src/ui/recordingControls.js b/src/ui/recordingControls.js index f3e30fc06..7ddaf41e6 100644 --- a/src/ui/recordingControls.js +++ b/src/ui/recordingControls.js @@ -1,7 +1,16 @@ -/** Own recording chrome, safe frames and the pre-recording HUD snapshot. */ +/** + * Own recording chrome, safe frames and the pre-recording HUD snapshot. The + * safe frame a recording composes in goes to `publishSafeFrame` ('16:9', + * '9:16', or null while none records); the scene chrome renders it. + */ export class RecordingControls { - constructor({ syncShareState, syncHudControls }) { + constructor({ + syncShareState, + syncHudControls, + publishSafeFrame = () => {}, + }) { this._syncShareState = syncShareState; + this._publishSafeFrame = publishSafeFrame; // Shows the HUD's visibility and layout on the Display controls. this._syncHudControls = syncHudControls; this.destroyed = false; @@ -12,18 +21,10 @@ export class RecordingControls { safeFrame: '16:9', }; this._preRecordingHudState = null; - this._safeFrameOverlay = document.getElementById('safe-frame-overlay'); - this._safeFrameBox = document.getElementById('safe-frame-box'); this.hud = null; } _initRecordingOverlay() { - if (this.destroyed) return; - if (!this._safeFrameOverlay || !this._safeFrameBox) return; - this._safeFrameOverlay.classList.remove( - 'active', - 'ratio-9-16', - 'ratio-16-9', - ); + if (!this.destroyed) this._publishSafeFrame(null); } setRecordingMode(enabled, options = {}) { @@ -41,13 +42,7 @@ export class RecordingControls { this._recordingMode && hidePanels, ); - if (this._safeFrameOverlay) { - this._safeFrameOverlay.classList.remove('ratio-9-16', 'ratio-16-9'); - this._safeFrameOverlay.classList.toggle('active', this._recordingMode); - this._safeFrameOverlay.classList.add( - safeFrame === '9:16' ? 'ratio-9-16' : 'ratio-16-9', - ); - } + this._publishSafeFrame(this._recordingMode ? (safeFrame ?? '16:9') : null); if (this._recordingMode) { // Snapshot the user's HUD state once per recording session so exit can @@ -73,13 +68,6 @@ export class RecordingControls { this.hud.setVariant(saved.variant); } this.hud.setMode(saved ? saved.mode : 'auto'); - if (this._safeFrameOverlay) { - this._safeFrameOverlay.classList.remove( - 'active', - 'ratio-9-16', - 'ratio-16-9', - ); - } } this._syncHudControls(); this._syncShareState(); diff --git a/src/ui/safeFrameView.test.mjs b/src/ui/safeFrameView.test.mjs new file mode 100644 index 000000000..6ba445a63 --- /dev/null +++ b/src/ui/safeFrameView.test.mjs @@ -0,0 +1,59 @@ +// src/ui/safeFrameView.test.mjs — the recording safe frame renders what +// RecordingControls publishes to the recording store. +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 { useRecordingStore } from './stores/recording.js'; +import { RecordingControls } from './recordingControls.js'; +import SceneChromeView from './views/SceneChromeView.vue'; + +test('the safe frame shows while a recording composes in it', async (t) => { + document.body.replaceChildren(); + const pinia = createPinia(); + const app = createApp(SceneChromeView).use(pinia); + app.mount(document.body.appendChild(document.createElement('div'))); + t.after(() => app.unmount()); + const store = useRecordingStore(pinia); + const recording = new RecordingControls({ + syncShareState() {}, + syncHudControls() {}, + publishSafeFrame: (frame) => store.publishSafeFrame(frame), + }); + recording.hud = { + getMode: () => 'auto', + getVariant: () => 'tactical', + setMode() {}, + setVariant() {}, + }; + t.after(() => recording.destroy()); + const overlay = document.getElementById('safe-frame-overlay'); + const classes = () => + ['active', 'ratio-16-9', 'ratio-9-16'].filter((name) => + overlay.classList.contains(name), + ); + recording._initRecordingOverlay(); + await nextTick(); + assert.deepEqual(classes(), []); + recording.setRecordingMode(true, { safeFrame: '9:16' }); + await nextTick(); + assert.deepEqual(classes(), ['active', 'ratio-9-16']); + recording.setRecordingMode(true); + await nextTick(); + assert.deepEqual(classes(), ['active', 'ratio-16-9']); + recording.setRecordingMode(false); + await nextTick(); + assert.deepEqual(classes(), []); + // Any frame but 9:16, even none named, records in 16:9. + recording.setRecordingMode(true, { safeFrame: null }); + await nextTick(); + assert.deepEqual(classes(), ['active', 'ratio-16-9']); + recording.setRecordingMode(true, { safeFrame: '4:3' }); + await nextTick(); + assert.deepEqual(classes(), ['active', 'ratio-16-9']); + // A recording that ends with its controls takes its frame with it. + recording.destroy(); + await nextTick(); + assert.deepEqual(classes(), []); +}); diff --git a/src/ui/shellElements.js b/src/ui/shellElements.js index c512532b2..4ed386a93 100644 --- a/src/ui/shellElements.js +++ b/src/ui/shellElements.js @@ -64,8 +64,6 @@ export function readShellElements(document = globalThis.document) { _cctvMeta: document.getElementById('cctv-meta'), _cctvSummary: document.getElementById('cctv-summary'), _locationSearch: document.getElementById('location-search'), - _safeFrameOverlay: document.getElementById('safe-frame-overlay'), - _safeFrameBox: document.getElementById('safe-frame-box'), _detectionBtn: document.getElementById('detection-toggle'), _models3dBtn: document.getElementById('models3d-toggle'), }; diff --git a/src/ui/shellLifecycle.test.mjs b/src/ui/shellLifecycle.test.mjs index 5773bfa96..f5746a4e6 100644 --- a/src/ui/shellLifecycle.test.mjs +++ b/src/ui/shellLifecycle.test.mjs @@ -56,8 +56,6 @@ function fixture() { 'left-panel-stack', 'right-context-rail', 'pp-toggles', - 'safe-frame-overlay', - 'safe-frame-box', 'hud-toggle', 'hud-layout-select', ].map((id) => [id, element()]), @@ -222,9 +220,11 @@ test('recording restores the original HUD after repeated entry and revokes retai let mode = 'auto', variant = 'tactical'; const hudSyncs = []; + const frames = []; const owner = new RecordingControls({ syncShareState() {}, syncHudControls: () => hudSyncs.push(variant), + publishSafeFrame: (frame) => frames.push(frame), }); owner.hud = { getMode: () => mode, @@ -249,9 +249,10 @@ test('recording restores the original HUD after repeated entry and revokes retai 'each change shows the HUD layout it leaves', ); assert.equal(document.body.classList.contains('recording-mode'), false); - assert.equal( - f.nodes.get('safe-frame-overlay').classList.contains('active'), - false, + assert.deepEqual( + frames, + ['9:16', '16:9', null], + 'the safe frame follows each change and leaves with the recording', ); } finally { f.restore(); diff --git a/src/ui/stores/recording.js b/src/ui/stores/recording.js new file mode 100644 index 000000000..13d131d1f --- /dev/null +++ b/src/ui/stores/recording.js @@ -0,0 +1,18 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +/** + * Recording composition as the scene chrome renders it. RecordingControls + * publishes the safe frame a recording composes in. + */ +export const useRecordingStore = defineStore('gev-recording', () => { + /** The recording's safe frame, '16:9' or '9:16'; null while none records. */ + const safeFrame = ref(null); + + /** @param {'16:9' | '9:16' | null} next */ + function publishSafeFrame(next) { + safeFrame.value = next == null ? null : next === '9:16' ? '9:16' : '16:9'; + } + + return { safeFrame, publishSafeFrame }; +}); diff --git a/src/ui/views/SceneChromeView.vue b/src/ui/views/SceneChromeView.vue index 3bfd34438..0656de0d7 100644 --- a/src/ui/views/SceneChromeView.vue +++ b/src/ui/views/SceneChromeView.vue @@ -3,6 +3,7 @@ import { storeToRefs } from 'pinia'; import { t } from '../../i18n/index.js'; import { useDisplayStore } from '../stores/display.js'; import { useLocationStore } from '../stores/location.js'; +import { useRecordingStore } from '../stores/recording.js'; import { useStatusStore } from '../stores/status.js'; import { useWorldOverlayActionsStore } from '../stores/worldOverlayActions.js'; import { vClassToggle } from '../rail/classToggle.js'; @@ -23,6 +24,8 @@ const { style } = storeToRefs(useDisplayStore()); // The status chips, toast and FPS readout show what ShellFeedback, // CctvControls, createToast() and createFrameRateMonitor() publish. const { loading, traffic, cctv, toast, fps } = storeToRefs(useStatusStore()); +// The safe frame a recording composes in, as RecordingControls publishes it. +const { safeFrame } = storeToRefs(useRecordingStore()); // 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. @@ -98,7 +101,7 @@ const vToastText = {
-
+