From ce343c1c6d0587b93e6aef4c68015237b5d4d10c Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 30 Oct 2026 22:05:41 -0700 Subject: [PATCH] feat(ui): render the CCTV camera picker from the store The picker's options are data: CctvControls computes the cameras in view and the closing note for those left out, keeps a focused picker's list and prepends a camera activated under it, and publishes the options, disabled state and selection. CctvPicker renders them keyed by camera id and applies the selection once the options are in place; it updates only when the controls publish, as the old picker did on layer notifications. A picker about to be disabled no longer counts as focused. This completes the CCTV panel: everything it shows is rendered from the gev-cctv store. --- docs/VUE-MIGRATION.md | 9 +- scripts/package-boundaries.json | 1 + src/data/cctv.test.mjs | 23 --- src/ui/cctv/CctvPicker.vue | 32 ++++ src/ui/cctvBindings.js | 2 +- src/ui/cctvPanel.test.mjs | 15 -- src/ui/cctvPicker.test.mjs | 234 +++++++++++++++++++----------- src/ui/cctvPresentation.js | 115 +++++++-------- src/ui/stores/cctv.js | 15 +- src/ui/views/panels/CctvPanel.vue | 7 +- 10 files changed, 257 insertions(+), 196 deletions(-) create mode 100644 src/ui/cctv/CctvPicker.vue diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index d0f85c994..4424b73c1 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -86,10 +86,11 @@ They contain no application state. (done: `RecentImageryReadout` and its day cards, the same way), Street level (done: `StreetLevelPanel` renders the `gev-street-level` store that `StreetLevelControls` publishes to; the controls keep their - listeners, EXPAND, focus and resizing), CCTV (in progress: `CctvPanel` - renders the whole panel from the `gev-cctv` store, including the inline - pose editor, the frame preview, source badge and typed summary; - `CctvControls` still fills the picker's options), Layers, Display, Context and Radio, the command dock, + listeners, EXPAND, focus and resizing), CCTV (done: `CctvPanel` renders + the whole panel from the `gev-cctv` store, including the camera picker, + the inline pose editor, the frame preview, source badge and typed + summary; `CctvControls` keeps its listeners, the frame requests and the + live video surface), Layers, Display, Context and Radio, the command dock, the phone shell, then Cockpit. Layers that write into the right rail directly gain a slot instead. diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index e8bfd3bad..d9041aebd 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -48,6 +48,7 @@ "src/ui/views/panels/StreetLevelPanel.vue", "src/ui/views/panels/CctvPanel.vue", "src/ui/cctv/CctvCalValue.vue", + "src/ui/cctv/CctvPicker.vue", "src/ui/stores/cctv.js", "src/ui/stores/streetLevel.js", "src/ui/rail/RailChips.vue" diff --git a/src/data/cctv.test.mjs b/src/data/cctv.test.mjs index e7bc7f72b..f76acdb72 100644 --- a/src/data/cctv.test.mjs +++ b/src/data/cctv.test.mjs @@ -25,9 +25,6 @@ function componentFunctionSource(fn) { .replace(/\(\s+/g, '('); } import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import * as Cesium from 'cesium'; import cctvLayer, { CCTV_PROJECTION_OVERLAY_SOURCE_OPTIONS, @@ -83,16 +80,6 @@ import { LIVE_CAMERA_COLOR, } from '../layers/cctv/policy.js'; -const CCTV_PRESENTATION_SOURCE = fs.readFileSync( - path.join( - path.dirname(fileURLToPath(import.meta.url)), - '..', - 'ui', - 'cctvPresentation.js', - ), - 'utf8', -); - const ASPECT = 16 / 9; const toRad = (deg) => (deg * Math.PI) / 180; const GESTURE_TYPES = { @@ -1302,16 +1289,6 @@ test('CCTV null-active coverage, auto-hop, cycling, and panel targets stay hones assert.equal(cctvCycleIndex(-1, -1, records.length), records.length - 1); assert.equal(cctvCycleIndex(2, 1, records.length), 0); assert.equal(cctvCycleIndex(0, -1, records.length), records.length - 1); - - const renderer = CCTV_PRESENTATION_SOURCE.match( - /_renderCctvState\(state\) \{[\s\S]*?\n\}\n/, - ); - assert.ok(renderer, '_renderCctvState is missing'); - assert.match( - renderer[0], - // Nothing active clears the pick; only the outside-view note may show. - /else if \(!activeId\)[\s\S]*?selectedIndex =[^;]*\? 0 : -1;/, - ); } finally { cctvLayer.setParams({ autoHop: false }); _setCctvOverlayHostForTest(); diff --git a/src/ui/cctv/CctvPicker.vue b/src/ui/cctv/CctvPicker.vue new file mode 100644 index 000000000..719209cb2 --- /dev/null +++ b/src/ui/cctv/CctvPicker.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/ui/cctvBindings.js b/src/ui/cctvBindings.js index 717770989..d9492d870 100644 --- a/src/ui/cctvBindings.js +++ b/src/ui/cctvBindings.js @@ -61,7 +61,7 @@ export function _initCctvPanel() { // The picker defers rebuilds while focused; catch up once it lets go. this.listen(this._cctvSelect, 'blur', () => { - this._cctvSelectSource = null; + this._picker = null; this._renderCctvState(this._cctvState); }); diff --git a/src/ui/cctvPanel.test.mjs b/src/ui/cctvPanel.test.mjs index 66771a103..f3e0223f6 100644 --- a/src/ui/cctvPanel.test.mjs +++ b/src/ui/cctvPanel.test.mjs @@ -149,21 +149,6 @@ test('the buttons stay the elements the controls listen on', async (t) => { assert.equal(byId('cctv-nearest-btn').disabled, true); }); -test('re-renders leave the picker options the controls fill alone', async (t) => { - const { controls, byId } = panel(t); - const select = byId('cctv-camera-select'); - const option = document.createElement('option'); - option.value = 'a'; - option.textContent = 'Austin · Congress Ave'; - select.append(option); - for (const enabled of [true, false, true]) { - controls._renderCctvState({ enabled, count: 1, cameraIndex: [] }); - await nextTick(); - } - assert.equal(select.options.length, 1); - assert.equal(select.options[0].value, 'a'); -}); - test('FOCUS waits for an active camera while the other buttons work', () => { const loaded = { enabled: true, count: 2, cameraIndex: [] }; const view = presentCctvPanel(loaded, true); diff --git a/src/ui/cctvPicker.test.mjs b/src/ui/cctvPicker.test.mjs index c96fdeacb..7e36c7291 100644 --- a/src/ui/cctvPicker.test.mjs +++ b/src/ui/cctvPicker.test.mjs @@ -1,123 +1,116 @@ // src/ui/cctvPicker.test.mjs — the camera picker lists the cameras in view and // says how many more the view leaves out. +import '../testSupport/registerDom.mjs'; import test from 'node:test'; import assert from 'node:assert/strict'; +import { createPinia } from 'pinia'; +import { createApp, nextTick } from 'vue'; import { CctvControls } from './cctvControls.js'; - -/** The slice of a +