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 = {
-