From b870df4199c002b69c5cfeaf2d07caeab645596e Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 30 Oct 2026 23:45:03 -0700 Subject: [PATCH] feat(ui): render the Display panel's Draw controls from the store --- docs/VUE-MIGRATION.md | 9 ++++--- src/annotations/drawTool.js | 29 ++++++++++------------ src/annotations/drawTool.test.mjs | 6 +++-- src/annotations/drawToolBehaviour.test.mjs | 10 ++++++-- src/app/tools.js | 9 ++++++- src/ui/displayPanel.test.mjs | 27 ++++++++++++++++++++ src/ui/geoFileImport.test.mjs | 4 +-- src/ui/stores/display.js | 15 ++++++++--- src/ui/views/panels/DisplayPanel.vue | 25 +++++++++++-------- 9 files changed, 92 insertions(+), 42 deletions(-) diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 64a2ba93d..621c1fe06 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -96,10 +96,11 @@ They contain no application state. descriptor before acting), Display (in progress: `DisplayPanel` renders the HUD, Sonar, Scope, Celestial, detection, bloom and sharpen from the `gev-display` store, which VisualSettings publishes to, and the 3D - aircraft group, which AircraftDisplay publishes beside it; the style - parameter rows render through Vue behind `createStyleParameters()`, as a - render function so the portable `./ui/input` entry needs no component - compiler; the other groups follow one owner at a time), + aircraft group and the Draw controls, which AircraftDisplay and the draw + tool publish beside it; the style parameter rows render through Vue + behind `createStyleParameters()`, as a render function so the portable + `./ui/input` entry needs no component compiler; the other groups follow + one owner at a time), 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/src/annotations/drawTool.js b/src/annotations/drawTool.js index 9467406dd..7cd1fa895 100644 --- a/src/annotations/drawTool.js +++ b/src/annotations/drawTool.js @@ -67,17 +67,17 @@ const PREVIEW = { /** * Wire the Draw control. Returns a handle with a `destroy()` the application * lifetime owns, plus the console/test seam (`window.__gevDrawTool`). - * @param {{viewer: Cesium.Viewer, annotations: {annotate: Function, clear: Function}}} deps + * The tool keeps its listeners on the controls and hands what they show + * (`{ active, shape, hint }`) to `publish`, which the view renders. + * @param {{viewer: Cesium.Viewer, annotations: {annotate: Function, clear: Function}, publish?: Function}} deps * @returns {{destroy: Function}|null} */ -export function initDrawTool({ viewer, annotations }) { +export function initDrawTool({ viewer, annotations, publish = () => {} }) { const toggle = document.getElementById('draw-toggle'); const modeRow = document.getElementById('draw-mode-row'); - const labelRow = document.getElementById('draw-label-row'); const labelInput = document.getElementById('draw-label-input'); const colorSelect = document.getElementById('draw-color-select'); const clearButton = document.getElementById('draw-clear'); - const hint = document.getElementById('draw-hint'); if (!viewer || !annotations || !toggle) return null; let active = false; @@ -85,6 +85,7 @@ export function initDrawTool({ viewer, annotations }) { let session = null; let shape = 'area'; let color = 'primary'; + let hint = ''; let handler = null; let lease = null; let savedDoubleClick = null; @@ -124,8 +125,10 @@ export function initDrawTool({ viewer, annotations }) { () => null, ); + const present = () => publish({ active, shape, hint }); const setHint = (text) => { - if (hint) hint.textContent = text; + hint = text; + present(); }; // ---- preview --------------------------------------------------------- @@ -354,10 +357,7 @@ export function initDrawTool({ viewer, annotations }) { } } active = next; - toggle.classList.toggle('active', active); - toggle.setAttribute('aria-pressed', String(active)); - modeRow?.classList.toggle('visible', active); - labelRow?.classList.toggle('visible', active); + present(); document.body.classList.toggle('gev-drawing', active); if (active) { session = createDrawSession(shape); @@ -433,13 +433,13 @@ export function initDrawTool({ viewer, annotations }) { const shapeButtons = [ ...(modeRow?.querySelectorAll('.pp-mode-btn[data-shape]') || []), ]; - const shapeControl = createSegmentedControl({ + createSegmentedControl({ buttons: shapeButtons, valueOf: (button) => button.dataset.shape, listen, onChange: (value) => { shape = normalizeShape(value); - shapeControl.update(shape); + present(); if (active) { generation += 1; session = createDrawSession(shape); @@ -546,11 +546,8 @@ export function initDrawTool({ viewer, annotations }) { } }); document.body.classList.remove('gev-drawing'); - toggle.classList.remove('active'); - toggle.setAttribute('aria-pressed', 'false'); - modeRow?.classList.remove('visible'); - labelRow?.classList.remove('visible'); - if (hint) hint.textContent = ''; + hint = ''; + present(); if (window.__gevDrawTool === api) delete window.__gevDrawTool; return attaching; }, diff --git a/src/annotations/drawTool.test.mjs b/src/annotations/drawTool.test.mjs index 094b07b06..f4b238542 100644 --- a/src/annotations/drawTool.test.mjs +++ b/src/annotations/drawTool.test.mjs @@ -23,7 +23,7 @@ test('the application composition wires the draw tool and owns its teardown', () const tools = read('src/app/tools.js'); const engineAt = tools.indexOf('const annotations = initAnnotations({'); const drawAt = tools.indexOf( - 'const drawTool = initDrawTool({ viewer, annotations });', + 'const drawTool = initDrawTool({\n viewer,\n annotations,\n publish: (view) => displayStore.publishDraw(view),', ); assert.ok(engineAt >= 0, 'initAnnotations call missing'); assert.ok( @@ -88,7 +88,9 @@ test('the Draw control markup carries the ids the tool binds to', () => { `shape button ${shape} missing`, ); } - for (const id of ids) { + // The rows and the hint render what the tool publishes; it binds the rest. + const bound = ids.filter((id) => id !== 'draw-label-row' && id !== 'draw-hint'); + for (const id of bound) { assert.match( tool(), new RegExp(`getElementById\\('${id}'\\)`), diff --git a/src/annotations/drawToolBehaviour.test.mjs b/src/annotations/drawToolBehaviour.test.mjs index f8b9afcd1..c63c9e9ee 100644 --- a/src/annotations/drawToolBehaviour.test.mjs +++ b/src/annotations/drawToolBehaviour.test.mjs @@ -218,9 +218,15 @@ function harness() { const previousWindow = globalThis.window; globalThis.document = documentDouble; globalThis.window = globalThis.window || {}; - const tool = initDrawTool({ viewer, annotations }); + const shown = { view: null }; + const tool = initDrawTool({ + viewer, + annotations, + publish: (view) => (shown.view = view), + }); return { tool, + shown, viewer, annotations, byId, @@ -289,7 +295,7 @@ test('Draw refuses to start when something else holds the pointer', async () => assert.equal(pointerOwner(), 'directions', 'and must not steal the claim'); // Named by its tool, not by the internal owner id. assert.equal( - h.element('draw-hint').textContent, + h.shown.view.hint, 'Directions is using the pointer — close it first.', ); assert.equal(h.tool.diagnostics().sceneHandler, false, 'no handler was bound'); diff --git a/src/app/tools.js b/src/app/tools.js index 7aea39c22..c200abbba 100644 --- a/src/app/tools.js +++ b/src/app/tools.js @@ -2,6 +2,8 @@ import { SceneDirector } from '../scenes/director.js'; import { initAnnotations } from '../annotations/index.js'; import { initDrawTool } from '../annotations/drawTool.js'; import { initGeoFileImport } from '../ui/geoFileImport.js'; +import { useDisplayStore } from '../ui/stores/display.js'; +import { applicationPinia } from '../ui/vueRuntime.js'; import { initImageryBoxTool } from '../ui/imageryBoxTool.js'; import { createRecentImageryPanel } from '../ui/recentImagery.js'; import { initGevVoiceCommands } from '../voice/gevRealtime.js'; @@ -56,7 +58,12 @@ export function createApplicationTools({ // DISPLAY ▸ Draw: the same whiteboard, drawn by hand. It claims the pointer // while a session is open, so its teardown belongs to the application // lifetime rather than to whoever last pressed the button. - const drawTool = initDrawTool({ viewer, annotations }); + const displayStore = useDisplayStore(applicationPinia()); + const drawTool = initDrawTool({ + viewer, + annotations, + publish: (view) => displayStore.publishDraw(view), + }); defer(() => drawTool?.destroy()); // DISPLAY ▸ Import: the person's own GeoJSON/KML/KMZ/GPX files, each its // own data source. They live for the page session, so the application diff --git a/src/ui/displayPanel.test.mjs b/src/ui/displayPanel.test.mjs index bc225c992..b666774f5 100644 --- a/src/ui/displayPanel.test.mjs +++ b/src/ui/displayPanel.test.mjs @@ -343,3 +343,30 @@ test('the 3D aircraft group renders what AircraftDisplay shows', async (t) => { assert.equal(row.classList.contains('visible'), true); assert.equal(all.getAttribute('aria-checked'), 'false'); }); + +test('the Draw controls render what the draw tool publishes', async (t) => { + const { store, $ } = fixture(t); + const toggle = $('draw-toggle'); + const segment = (shape) => + document.querySelector(`#draw-mode-row [data-shape="${shape}"]`); + assert.equal(toggle.getAttribute('aria-pressed'), 'false'); + assert.equal(segment('area').getAttribute('aria-checked'), 'true'); + assert.equal($('draw-hint').textContent, ''); + + store.publishDraw({ active: true, shape: 'line', hint: 'Click to add points.' }); + await nextTick(); + assert.equal(toggle.className, 'pp-toggle-btn active'); + assert.equal(toggle.getAttribute('aria-pressed'), 'true'); + assert.equal($('draw-mode-row').classList.contains('visible'), true); + assert.equal($('draw-label-row').classList.contains('visible'), true); + assert.equal(segment('line').getAttribute('aria-checked'), 'true'); + assert.equal(segment('line').classList.contains('active'), true); + assert.equal(segment('area').classList.contains('active'), false); + assert.equal($('draw-hint').textContent, 'Click to add points.'); + + store.publishDraw({ active: false, shape: 'line', hint: '' }); + await nextTick(); + assert.equal(toggle.className, 'pp-toggle-btn'); + assert.equal($('draw-mode-row').classList.contains('visible'), false); + assert.equal($('draw-label-row').classList.contains('visible'), false); +}); diff --git a/src/ui/geoFileImport.test.mjs b/src/ui/geoFileImport.test.mjs index a01193dde..ee66bf84f 100644 --- a/src/ui/geoFileImport.test.mjs +++ b/src/ui/geoFileImport.test.mjs @@ -427,9 +427,7 @@ test('the application composition wires the import and owns its teardown', () => new URL('../app/tools.js', import.meta.url), 'utf8', ); - const drawAt = tools.indexOf( - 'const drawTool = initDrawTool({ viewer, annotations });', - ); + const drawAt = tools.indexOf('const drawTool = initDrawTool({'); const importAt = tools.indexOf('const geoFileImport = initGeoFileImport({'); assert.ok(importAt > drawAt && drawAt >= 0); // Fly-to goes through the application shell's navigation policy. diff --git a/src/ui/stores/display.js b/src/ui/stores/display.js index 9e6db0804..53c904c7d 100644 --- a/src/ui/stores/display.js +++ b/src/ui/stores/display.js @@ -3,8 +3,9 @@ import { markRaw, shallowRef } from 'vue'; /** * The Display panel as its view renders it. VisualSettings presents its - * groups (`_presentDisplay()`) and AircraftDisplay the 3D aircraft group, - * and each publishes here; both keep their listeners on the controls. + * groups (`_presentDisplay()`), AircraftDisplay the 3D aircraft group and + * the draw tool its controls, and each publishes here; each keeps its + * listeners on the controls. */ export const useDisplayStore = defineStore('gev-display', () => { /** The presented panel; null until VisualSettings first renders. */ @@ -23,5 +24,13 @@ export const useDisplayStore = defineStore('gev-display', () => { models3d.value = markRaw(next); } - return { view, publish, models3d, publishModels3d }; + /** The Draw controls; null until the draw tool first renders. */ + const draw = shallowRef(null); + + /** @param {object} next A fresh presentation of the Draw controls. */ + function publishDraw(next) { + draw.value = markRaw(next); + } + + return { view, publish, models3d, publishModels3d, draw, publishDraw }; }); diff --git a/src/ui/views/panels/DisplayPanel.vue b/src/ui/views/panels/DisplayPanel.vue index 9534c294c..6492fd2f8 100644 --- a/src/ui/views/panels/DisplayPanel.vue +++ b/src/ui/views/panels/DisplayPanel.vue @@ -6,10 +6,11 @@ import { vRangeValue } from '../../rail/rangeValue.js'; import { useDisplayStore } from '../../stores/display.js'; // The Display panel. VisualSettings presents the HUD, Sonar, Scope, -// Celestial, detection, bloom and sharpen and AircraftDisplay the 3D group, -// and each publishes them; until they do, the panel shows its initial -// markup. The other groups keep their owners' element bindings. -const { view, models3d } = storeToRefs(useDisplayStore()); +// Celestial, detection, bloom and sharpen, AircraftDisplay the 3D group and +// the draw tool its controls, and each publishes them; until they do, the +// panel shows its initial markup. The other groups keep their owners' +// element bindings. +const { view, models3d, draw } = storeToRefs(useDisplayStore()); const pct = (value) => `${value}%`; const hud = computed(() => view.value?.hud ?? null); const sonar = computed(() => view.value?.sonar ?? null); @@ -33,6 +34,8 @@ const segment = (current) => (value) => ({ 'aria-checked': String(current.value === value), }); const modelsSegment = segment(modelsMode); +const drawShape = computed(() => draw.value?.shape ?? 'area'); +const drawSegment = segment(drawShape); const allocationSegment = segment(allocation); @@ -156,19 +159,19 @@ const allocationSegment = segment(allocation);
- -
+
Shape
- - - + + +
-
+
- + {{ draw?.hint ?? '' }}