From c99c0e8000bf385ef52ba12745d96e7b23bc6e58 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 30 Oct 2026 22:51:47 -0700 Subject: [PATCH] feat(ui): render the Display panel's detection controls from the store --- docs/VUE-MIGRATION.md | 2 +- src/sharelink.celestial.test.mjs | 4 +- src/ui/applicationShell.js | 22 +- src/ui/cyberTheme.test.mjs | 2 +- src/ui/displayBindings.js | 22 +- src/ui/displayControls.js | 3 +- src/ui/displayControls.test.mjs | 6 +- src/ui/displayPanel.test.mjs | 84 +++++++ src/ui/shellElements.js | 13 +- src/ui/shellFacade.js | 29 ++- src/ui/views/panels/DisplayPanel.vue | 48 ++-- src/ui/visualSettings.js | 337 ++++++++++----------------- 12 files changed, 273 insertions(+), 299 deletions(-) diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index b946286c8..bf8b17438 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -94,7 +94,7 @@ They contain no application state. renders `LayerRows` synchronously; keyed chips and list items keep focus through the refresh their own click triggers, and clicks re-read the live descriptor before acting), Display (in progress: `DisplayPanel` renders - bloom and sharpen from the `gev-display` store, which VisualSettings + bloom, sharpen and detection from the `gev-display` store, which VisualSettings publishes to; 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 diff --git a/src/sharelink.celestial.test.mjs b/src/sharelink.celestial.test.mjs index 054e6d035..71c656476 100644 --- a/src/sharelink.celestial.test.mjs +++ b/src/sharelink.celestial.test.mjs @@ -524,9 +524,9 @@ test('every explicit visual UI gesture claims restore authority before it mutate ['setScopeFeather:', 'setSharpenIntensity:', 'setScopeMaskFeather(', 'setScopeFeather control'], ['setSharpenIntensity:', 'setHudLayout:', 'this._setSharpenIntensity(', 'setSharpenIntensity control'], ['setHudLayout:', 'toggleCleanView:', 'this._setHudVariant(', 'setHudLayout control'], - ['setDensity:', 'setAllocation:', 'this._applyDetectionDensityFromUi()', 'setDensity control'], + ['setDensity:', 'setAllocation:', 'this._setDetectionDensity(', 'setDensity control'], ['setAllocation:', 'setFade:', 'this._setDetectionAllocation(', 'setAllocation control'], - ['setFade:', 'toggleCelestial:', 'this._applyDetectionFadeFromUi()', 'setFade control'], + ['setFade:', 'toggleCelestial:', 'this._setDetectionFade(', 'setFade control'], ]; for (const [start, end, mutation, label] of gestureRoutes) { const startIndex = initUi.indexOf(start); diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index 5f9b11f8a..4c4a0f84d 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -209,17 +209,6 @@ export class StyleManager extends ShellFacade { elements: { _celestialBtn: this._celestialBtn, _cockpitDisplayToggleBtn: this._cockpitDisplayToggleBtn, - _detectionAllocationRow: this._detectionAllocationRow, - _detectionBtn: this._detectionBtn, - _detectionDensitySlider: this._detectionDensitySlider, - _detectionDensityValue: this._detectionDensityValue, - _detectionFadeRow: this._detectionFadeRow, - _detectionFadeSlider: this._detectionFadeSlider, - _detectionFadeValue: this._detectionFadeValue, - _detectionOpacityRow: this._detectionOpacityRow, - _detectionOpacitySlider: this._detectionOpacitySlider, - _detectionOpacityValue: this._detectionOpacityValue, - _detectionSliderRow: this._detectionSliderRow, _hudBtn: this._hudBtn, _cyberSonarBtn: this._cyberSonarBtn, _cyberSonarRings: this._cyberSonarRings, @@ -507,7 +496,7 @@ export class StyleManager extends ShellFacade { ); initTrackedReadout(viewer); setDetectionStyle(this.activeStyle); - this._applyDetectionDensityFromUi(); + this._setDetectionDensity(); this._initStages(); this._initBloomSharpen(); @@ -541,10 +530,10 @@ export class StyleManager extends ShellFacade { _detectionBtn: this._detectionBtn, _detectionFadeSlider: this._detectionFadeSlider, _detectionOpacitySlider: this._detectionOpacitySlider, + _detectionAllocationBtns: this._detectionAllocationBtns, _celestialBtn: this._celestialBtn, _models3dBtn: this._models3dBtn, _scopeFeatherValue: this._scopeFeatherValue, - _detectionDensityValue: this._detectionDensityValue, }, operations: { setStyle: (...args) => this.setStyle(...args), @@ -560,12 +549,10 @@ export class StyleManager extends ShellFacade { _setHudVariant: (...args) => this._setHudVariant(...args), _setCyberSonarEnabled: (...args) => this._setCyberSonarEnabled(...args), _setCyberSonarSetting: (...args) => this._setCyberSonarSetting(...args), - _applyDetectionDensityFromUi: (...args) => - this._applyDetectionDensityFromUi(...args), + _setDetectionDensity: (...args) => this._setDetectionDensity(...args), _setDetectionAllocation: (...args) => this._setDetectionAllocation(...args), - _applyDetectionFadeFromUi: (...args) => - this._applyDetectionFadeFromUi(...args), + _setDetectionFade: (...args) => this._setDetectionFade(...args), setCelestialRingEnabled: (...args) => this.setCelestialRingEnabled(...args), _setModels3dEnabled: (...args) => this._setModels3dEnabled(...args), @@ -581,7 +568,6 @@ export class StyleManager extends ShellFacade { celestialRingEnabled: this.celestialRingEnabled, _models3dEnabled: this._models3dEnabled, _models3dModeBtns: this._models3dModeBtns, - _detectionAllocationBtns: this._detectionAllocationBtns, }), claimDetection: () => { this._visualSettings._detectionUserOverridden = true; diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs index 6b0d036e8..67581f51c 100644 --- a/src/ui/cyberTheme.test.mjs +++ b/src/ui/cyberTheme.test.mjs @@ -93,7 +93,7 @@ function styleOwner(t, initialVariant = 'cyber') { '_syncCockpitInheritedStyle', '_syncCyberSonarControl', '_scheduleAdaptivePanelLayout', - '_applyDetectionFadeFromUi', + '_setDetectionFade', // Intensities: the fixture has no post-process stages to drive. '_setBloomIntensity', '_setSharpenIntensity', diff --git a/src/ui/displayBindings.js b/src/ui/displayBindings.js index 251c7040e..181f98eae 100644 --- a/src/ui/displayBindings.js +++ b/src/ui/displayBindings.js @@ -2,7 +2,6 @@ import { clampScopeFeatherPct } from '../scopeMaskPolicy.js'; import { createFrameRateMonitor } from './frameRateMonitor.js'; import { bindApplicationShortcuts } from './visualInput.js'; import { bindDisplayControls, syncScopeControls } from './displayControls.js'; -import { canonicalizeDensity } from '../data/detectionPolicy.js'; /** Own keyboard/display event subscriptions; settings remain with their state owners. */ export class DisplayBindings { @@ -45,9 +44,6 @@ export class DisplayBindings { get _models3dModeBtns() { return this.readState()._models3dModeBtns; } - get _detectionAllocationBtns() { - return this.readState()._detectionAllocationBtns; - } _initUI() { const { cycleDetectionMode, @@ -121,7 +117,8 @@ export class DisplayBindings { densitySlider: this._detectionDensitySlider, detectionButton: this._detectionBtn, allocationButtons: this._detectionAllocationBtns, - fadeSliders: [this._detectionFadeSlider, this._detectionOpacitySlider], + fadeSlider: this._detectionFadeSlider, + outsideOpacitySlider: this._detectionOpacitySlider, celestialButton: this._celestialBtn, modelsButton: this._models3dBtn, modelModeButtons: this._models3dBtn ? this._models3dModeBtns : [], @@ -167,11 +164,7 @@ export class DisplayBindings { setDensity: (value) => { this.shareLinkManager?.claimRestoreLane?.('visual'); this.claimDetection(); - const pct = canonicalizeDensity(value); - this._detectionDensitySlider.value = String(pct); - if (this._detectionDensityValue) - this._detectionDensityValue.textContent = `${pct}%`; - this._applyDetectionDensityFromUi(); + this._setDetectionDensity(value); this._syncShareState(); }, setAllocation: (value) => { @@ -179,9 +172,14 @@ export class DisplayBindings { this.claimDetection(); this._setDetectionAllocation(value); }, - setFade: () => { + setFade: (fadePct) => { this.shareLinkManager?.claimRestoreLane?.('visual'); - this._applyDetectionFadeFromUi(); + this._setDetectionFade({ fadePct }); + this._syncShareState(); + }, + setOutsideOpacity: (outsideOpacityPct) => { + this.shareLinkManager?.claimRestoreLane?.('visual'); + this._setDetectionFade({ outsideOpacityPct }); this._syncShareState(); }, toggleCelestial: () => { diff --git a/src/ui/displayControls.js b/src/ui/displayControls.js index ceab61d91..ffa009ed9 100644 --- a/src/ui/displayControls.js +++ b/src/ui/displayControls.js @@ -65,7 +65,8 @@ export function bindDisplayControls({ elements, actions }) { }); removers.push(() => control.destroy()); } - for (const el of elements.fadeSliders || []) listen(el, 'input', 'setFade'); + listen(elements.fadeSlider, 'input', 'setFade', integer); + listen(elements.outsideOpacitySlider, 'input', 'setOutsideOpacity', integer); return { destroy() { for (const remove of removers.splice(0)) remove(); diff --git a/src/ui/displayControls.test.mjs b/src/ui/displayControls.test.mjs index f7c5bcafb..4e44c582d 100644 --- a/src/ui/displayControls.test.mjs +++ b/src/ui/displayControls.test.mjs @@ -145,11 +145,11 @@ test('optional controls are absent safely and subscriptions stay instance-owned' const a = element(); const b = element(); const one = bindDisplayControls({ - elements: { fadeSliders: [null, a] }, - actions: { setFade: () => calls.push('a') }, + elements: { fadeSlider: null, outsideOpacitySlider: a }, + actions: { setOutsideOpacity: () => calls.push('a') }, }); const two = bindDisplayControls({ - elements: { fadeSliders: [b] }, + elements: { fadeSlider: b }, actions: { setFade: () => calls.push('b') }, }); one.destroy(); diff --git a/src/ui/displayPanel.test.mjs b/src/ui/displayPanel.test.mjs index 531ec97a6..5ed25f718 100644 --- a/src/ui/displayPanel.test.mjs +++ b/src/ui/displayPanel.test.mjs @@ -121,3 +121,87 @@ test('a publish never rewrites a slider the user is dragging', async (t) => { assert.equal(writes, 0); assert.equal(slider.value, '30'); }); + +/** VisualSettings with a stand-in detection engine. */ +function detectionSettings(store) { + const { owner } = settings(store); + const engine = { + mode: 'OFF', + tuning: { densityPct: 50, allocationStrategy: 'ELASTIC' }, + fade: { fadeRatio: 0.07, outsideOpacity: 0.01 }, + }; + Object.assign(owner, { + _detectionDensityPct: 50, + _detectionFadePct: 7, + _detectionOutsidePct: 1, + _detectionModeLabel: null, + _detectionAllocationShown: null, + viewer: { scene: { requestRender() {} } }, + }); + owner.services = { + ...owner.services, + getDetectionMode: () => engine.mode, + setDetectionModeByLabel: (label) => { + engine.mode = label; + }, + getDetectionTuning: () => ({ ...engine.tuning }), + setDetectionTuning: (next) => Object.assign(engine.tuning, next), + getKeyholeFadeTuning: () => ({ ...engine.fade }), + setKeyholeFadeTuning: (next) => Object.assign(engine.fade, next), + }; + return { owner, engine }; +} + +test('detection renders its mode, rows, sliders and allocation', async (t) => { + const { store, $ } = fixture(t); + const { owner, engine } = detectionSettings(store); + const button = $('detection-toggle'); + assert.equal(button.querySelector('.pp-label').textContent, 'DETECT'); + assert.equal(button.getAttribute('aria-label'), 'Detection overlay'); + + owner._setDetectionMode('DENSE'); + owner._setDetectionDensity(80); + await nextTick(); + assert.equal(button.querySelector('.pp-label').textContent, 'DENSE'); + assert.equal(button.getAttribute('aria-pressed'), 'true'); + assert.equal(button.getAttribute('aria-label'), 'Detection overlay: dense'); + assert.equal(button.className, 'pp-toggle-btn active panoptic'); + for (const id of [ + 'detection-slider-row', + 'detection-allocation-row', + 'detection-fade-row', + 'detection-opacity-row', + ]) + assert.equal($(id).classList.contains('visible'), true, id); + assert.equal($('detection-density-slider').value, '75', 'nearest stop'); + assert.equal($('detection-density-value').textContent, '75%'); + assert.equal(engine.tuning.densityPct, 75); + + owner._setDetectionFade({ fadePct: 55 }); + owner._setDetectionAllocation('weighted', { persist: false }); + await nextTick(); + assert.equal($('detection-fade-slider').value, '40', 'clamped to 40'); + assert.equal($('detection-fade-value').textContent, '40%'); + assert.equal($('detection-opacity-value').textContent, '1%', 'kept'); + assert.equal(engine.fade.fadeRatio, 0.4); + const weighted = $('detection-allocation-weighted'); + const elastic = $('detection-allocation-elastic'); + assert.equal(weighted.classList.contains('active'), true); + assert.equal(weighted.getAttribute('aria-checked'), 'true'); + assert.equal(elastic.classList.contains('active'), false); + assert.equal(elastic.getAttribute('aria-checked'), 'false'); + assert.deepEqual(owner.getDetectionState(), { + detectionMode: 'DENSE', + densityPct: 75, + allocationStrategy: 'WEIGHTED', + fadePct: 40, + outsideOpacityPct: 1, + }); + + owner._setDetectionMode('OFF'); + await nextTick(); + assert.equal(button.querySelector('.pp-label').textContent, 'DETECT'); + assert.equal(button.getAttribute('aria-label'), 'Detection overlay: off'); + assert.equal(button.className, 'pp-toggle-btn'); + assert.equal($('detection-slider-row').classList.contains('visible'), false); +}); diff --git a/src/ui/shellElements.js b/src/ui/shellElements.js index fd7c3e27c..683987c4f 100644 --- a/src/ui/shellElements.js +++ b/src/ui/shellElements.js @@ -28,22 +28,17 @@ export function readShellElements(document = globalThis.document) { ), _cyberSonarSector: document.getElementById('cyber-sonar-sector'), _cyberSonarSectorValue: document.getElementById('cyber-sonar-sector-value'), - _detectionSliderRow: document.getElementById('detection-slider-row'), _detectionDensitySlider: document.getElementById( 'detection-density-slider', ), - _detectionDensityValue: document.getElementById('detection-density-value'), - _detectionAllocationRow: document.getElementById( - 'detection-allocation-row', - ), - _detectionFadeRow: document.getElementById('detection-fade-row'), + _detectionAllocationBtns: [ + document.getElementById('detection-allocation-elastic'), + document.getElementById('detection-allocation-weighted'), + ].filter(Boolean), _detectionFadeSlider: document.getElementById('detection-fade-slider'), - _detectionFadeValue: document.getElementById('detection-fade-value'), - _detectionOpacityRow: document.getElementById('detection-opacity-row'), _detectionOpacitySlider: document.getElementById( 'detection-opacity-slider', ), - _detectionOpacityValue: document.getElementById('detection-opacity-value'), _celestialBtn: document.getElementById('celestial-toggle'), _scopeBtn: document.getElementById('scope-toggle'), _scopeFeatherSlider: document.getElementById('scope-feather-slider'), diff --git a/src/ui/shellFacade.js b/src/ui/shellFacade.js index 5cc7772bf..4f44d3039 100644 --- a/src/ui/shellFacade.js +++ b/src/ui/shellFacade.js @@ -429,14 +429,6 @@ export class ShellFacade { this._visualSettings._contactsDetectionRestore = value; } - get _detectionAllocationBtns() { - return this._visualSettings._detectionAllocationBtns; - } - - set _detectionAllocationBtns(value) { - this._visualSettings._detectionAllocationBtns = value; - } - get _detectionAllocationPreference() { return this._visualSettings._detectionAllocationPreference; } @@ -716,17 +708,24 @@ export class ShellFacade { } /** - * Reads and canonicalizes the five-stop density control. The engine derives - * Sparse/Balanced/Dense from the same stop. + * Sets the five-stop density control, canonicalized; the engine derives + * Sparse/Balanced/Dense from the same stop. Without a value it reapplies + * the current stop. + * @param {number} [pct] * @returns {void} */ - _applyDetectionDensityFromUi() { - return this._visualSettings._applyDetectionDensityFromUi(...arguments); + _setDetectionDensity(pct) { + return this._visualSettings._setDetectionDensity(...arguments); } - /** Apply responsive keyhole fade controls from normalized UI percentages. */ - _applyDetectionFadeFromUi() { - return this._visualSettings._applyDetectionFadeFromUi(...arguments); + /** + * Sets the keyhole fade distance and outside opacity, in percent; either + * one left out keeps its current value. + * @param {{fadePct?: number, outsideOpacityPct?: number}} [tuning] + * @returns {void} + */ + _setDetectionFade(tuning) { + return this._visualSettings._setDetectionFade(...arguments); } _syncDetectionUiFromEngine() { diff --git a/src/ui/views/panels/DisplayPanel.vue b/src/ui/views/panels/DisplayPanel.vue index 8ef9c7198..edceb479b 100644 --- a/src/ui/views/panels/DisplayPanel.vue +++ b/src/ui/views/panels/DisplayPanel.vue @@ -1,12 +1,22 @@