diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 7f6d355de..fdc538b12 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -94,9 +94,9 @@ 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 - the HUD, Sonar, detection, bloom and sharpen 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 HUD, Sonar, Scope, Celestial, detection, bloom and sharpen 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 directly gain a slot instead. diff --git a/src/sharelink.celestial.test.mjs b/src/sharelink.celestial.test.mjs index 71c656476..105b8a573 100644 --- a/src/sharelink.celestial.test.mjs +++ b/src/sharelink.celestial.test.mjs @@ -520,8 +520,8 @@ test('every explicit visual UI gesture claims restore authority before it mutate ['toggleBloom:', 'setBloomIntensity:', 'this._setBloomEnabled(', 'toggleBloom control'], ['setBloomIntensity:', 'toggleSharpen:', 'this._setBloomIntensity(', 'setBloomIntensity control'], ['toggleSharpen:', 'setScopeEnabled:', 'this._setSharpenEnabled(', 'toggleSharpen control'], - ['setScopeEnabled:', 'setScopeFeather:', 'setScopeMaskEnabled(', 'setScopeEnabled control'], - ['setScopeFeather:', 'setSharpenIntensity:', 'setScopeMaskFeather(', 'setScopeFeather control'], + ['setScopeEnabled:', 'setScopeFeather:', 'this._setScope(', 'setScopeEnabled control'], + ['setScopeFeather:', 'setSharpenIntensity:', 'this._setScope(', 'setScopeFeather control'], ['setSharpenIntensity:', 'setHudLayout:', 'this._setSharpenIntensity(', 'setSharpenIntensity control'], ['setHudLayout:', 'toggleCleanView:', 'this._setHudVariant(', 'setHudLayout control'], ['setDensity:', 'setAllocation:', 'this._setDetectionDensity(', 'setDensity control'], diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index 23457616e..284d7dc01 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -208,12 +208,8 @@ export class StyleManager extends ShellFacade { setScopeMaskFeather: services.setScopeMaskFeather, }, elements: { - _celestialBtn: this._celestialBtn, _cockpitDisplayToggleBtn: this._cockpitDisplayToggleBtn, _ppToggles: this._ppToggles, - _scopeBtn: this._scopeBtn, - _scopeFeatherSlider: this._scopeFeatherSlider, - _scopeFeatherValue: this._scopeFeatherValue, _sliderContainer: this._sliderContainer, _sliderPanel: this._sliderPanel, _styleIndicator: this._styleIndicator, @@ -491,9 +487,7 @@ export class StyleManager extends ShellFacade { viewer, services: { cycleDetectionMode: services.cycleDetectionMode, - setScopeMaskEnabled: services.setScopeMaskEnabled, isScopeMaskEnabled: services.isScopeMaskEnabled, - setScopeMaskFeather: services.setScopeMaskFeather, }, elements: { _locationSearch: this._locationSearch, @@ -520,7 +514,6 @@ export class StyleManager extends ShellFacade { _detectionAllocationBtns: this._detectionAllocationBtns, _celestialBtn: this._celestialBtn, _models3dBtn: this._models3dBtn, - _scopeFeatherValue: this._scopeFeatherValue, }, operations: { setStyle: (...args) => this.setStyle(...args), @@ -540,6 +533,7 @@ export class StyleManager extends ShellFacade { _setDetectionAllocation: (...args) => this._setDetectionAllocation(...args), _setDetectionFade: (...args) => this._setDetectionFade(...args), + _setScope: (...args) => this._visualSettings._setScope(...args), setCelestialRingEnabled: (...args) => this.setCelestialRingEnabled(...args), _setModels3dEnabled: (...args) => this._setModels3dEnabled(...args), diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs index c33089fd0..ef0d1c8fa 100644 --- a/src/ui/cyberTheme.test.mjs +++ b/src/ui/cyberTheme.test.mjs @@ -279,28 +279,23 @@ test('scope choice and expanded feather survive every HUD layout and both restor const previous = { enabled: isScopeMaskEnabled(), feather: getScopeMaskFeather() }; t.after(() => { setScopeMaskEnabled(previous.enabled); setScopeMaskFeather(previous.feather); }); Object.assign(owner.services, { setScopeMaskEnabled, setScopeMaskFeather }); - owner._scopeBtn = { checked: true }; - owner._scopeFeatherSlider = { value: '11', disabled: false }; - owner._scopeFeatherValue = {}; + let shown = null; + owner.store = { publish: (view) => (shown = view.scope) }; await owner.restoreShareState({ scopeEnabled: false, scopeFeatherPct: 200 }); - assert.equal(owner._scopeBtn.checked, false); - assert.equal(owner._scopeFeatherSlider.disabled, true); - assert.equal(owner._scopeFeatherSlider.value, '200'); + assert.deepEqual(shown, { enabled: false, featherPct: 200 }); for (const layout of ['minimal', 'cyber', 'tactical']) { owner._setHudVariant(layout, { applyVisualDefaults: true }); assert.equal(isScopeMaskEnabled(), false, layout); assert.equal(getScopeMaskFeather(), 2, layout); - assert.equal(owner._scopeBtn.checked, false, layout); + assert.equal(shown.enabled, false, layout); } await owner.applyVisualState({ scope: { enabled: true, featherPct: 175 } }); assert.equal(isScopeMaskEnabled(), true); assert.equal(getScopeMaskFeather(), 1.75); - assert.equal(owner._scopeBtn.checked, true); - assert.equal(owner._scopeFeatherSlider.disabled, false); - assert.equal(owner._scopeFeatherValue.textContent, '175%'); + assert.deepEqual(shown, { enabled: true, featherPct: 175 }); await owner.applyVisualState({ scope: { featherPct: 500 } }); assert.equal(getScopeMaskFeather(), 2); - assert.equal(owner._scopeFeatherSlider.value, '200'); + assert.deepEqual(shown, { enabled: true, featherPct: 200 }); }); test('a scene shot saved with the retired Snow style plays on Normal', async (t) => { diff --git a/src/ui/displayBindings.js b/src/ui/displayBindings.js index 181f98eae..e2f213c02 100644 --- a/src/ui/displayBindings.js +++ b/src/ui/displayBindings.js @@ -1,7 +1,6 @@ -import { clampScopeFeatherPct } from '../scopeMaskPolicy.js'; import { createFrameRateMonitor } from './frameRateMonitor.js'; import { bindApplicationShortcuts } from './visualInput.js'; -import { bindDisplayControls, syncScopeControls } from './displayControls.js'; +import { bindDisplayControls } from './displayControls.js'; /** Own keyboard/display event subscriptions; settings remain with their state owners. */ export class DisplayBindings { @@ -45,12 +44,7 @@ export class DisplayBindings { return this.readState()._models3dModeBtns; } _initUI() { - const { - cycleDetectionMode, - setScopeMaskEnabled, - isScopeMaskEnabled, - setScopeMaskFeather, - } = this.services; + const { cycleDetectionMode, isScopeMaskEnabled } = this.services; this._applicationShortcuts?.destroy(); this._frameRateMonitor?.destroy(); this._frameRateMonitor = createFrameRateMonitor({ @@ -89,11 +83,7 @@ export class DisplayBindings { }, }); - syncScopeControls( - this._scopeBtn, - this._scopeFeatherSlider, - isScopeMaskEnabled(), - ); + this._setScope({ enabled: isScopeMaskEnabled() }, { apply: false }); this._displayControls?.destroy(); this._displayControls = bindDisplayControls({ elements: { @@ -139,16 +129,12 @@ export class DisplayBindings { }, setScopeEnabled: (enabled) => { this.shareLinkManager?.claimRestoreLane?.('visual'); - setScopeMaskEnabled(enabled); - syncScopeControls(this._scopeBtn, this._scopeFeatherSlider, enabled); + this._setScope({ enabled }); this._syncShareState(); }, setScopeFeather: (value) => { this.shareLinkManager?.claimRestoreLane?.('visual'); - const pct = clampScopeFeatherPct(value); - if (this._scopeFeatherValue) - this._scopeFeatherValue.textContent = `${pct}%`; - setScopeMaskFeather(pct / 100); + this._setScope({ featherPct: value }); this._syncShareState(); }, setSharpenIntensity: (pct) => { diff --git a/src/ui/displayControls.js b/src/ui/displayControls.js index ffa009ed9..3969e46f1 100644 --- a/src/ui/displayControls.js +++ b/src/ui/displayControls.js @@ -73,9 +73,3 @@ export function bindDisplayControls({ elements, actions }) { }, }; } - -/** Keep the native Scope checkbox and feather availability in step with the mask. */ -export function syncScopeControls(toggle, featherSlider, enabled) { - if (toggle) toggle.checked = enabled; - if (featherSlider) featherSlider.disabled = !enabled; -} diff --git a/src/ui/displayPanel.test.mjs b/src/ui/displayPanel.test.mjs index d0c70e614..47a0ab7da 100644 --- a/src/ui/displayPanel.test.mjs +++ b/src/ui/displayPanel.test.mjs @@ -253,3 +253,47 @@ test('the HUD and Sonar controls render what VisualSettings shows', async (t) => assert.equal($('cyber-sonar-sector').value, '30'); assert.equal($('cyber-sonar-sector-value').textContent, '30°'); }); + +test('Scope and Celestial render what VisualSettings shows', async (t) => { + const { store, $ } = fixture(t); + const { owner } = settings(store); + const mask = { enabled: true, feather: 0.35 }; + owner.services = { + ...owner.services, + setScopeMaskEnabled: (on) => (mask.enabled = on), + setScopeMaskFeather: (value) => (mask.feather = value), + }; + assert.equal($('scope-toggle').checked, true); + assert.equal($('scope-feather-value').textContent, '100%'); + assert.equal($('celestial-toggle').hasAttribute('aria-disabled'), false); + + owner._setScope({ enabled: false }, { apply: false }); + await nextTick(); + assert.equal($('scope-toggle').checked, false); + assert.equal($('scope-feather-slider').disabled, true); + assert.equal(mask.enabled, true, 'only shown'); + + owner._setScope({ enabled: true, featherPct: 250 }); + await nextTick(); + assert.equal($('scope-toggle').checked, true); + assert.equal($('scope-feather-slider').disabled, false); + assert.equal($('scope-feather-slider').value, '200', 'clamped'); + assert.equal($('scope-feather-value').textContent, '200%'); + assert.deepEqual(mask, { enabled: true, feather: 2 }); + + owner._celestialShown = { active: false, supported: false }; + owner._publishDisplay(); + await nextTick(); + const celestial = $('celestial-toggle'); + assert.equal(celestial.disabled, true); + assert.equal(celestial.getAttribute('aria-disabled'), 'true'); + assert.equal(celestial.title, 'Celestial ring — available in Normal style'); + + owner._celestialShown = { active: true, supported: true }; + owner._publishDisplay(); + await nextTick(); + assert.equal(celestial.disabled, false); + assert.equal(celestial.getAttribute('aria-pressed'), 'true'); + assert.equal(celestial.classList.contains('active'), true); + assert.equal(celestial.title, 'Celestial ring — reveal the full globe'); +}); diff --git a/src/ui/shellElements.js b/src/ui/shellElements.js index c1d02e31a..92f68ce36 100644 --- a/src/ui/shellElements.js +++ b/src/ui/shellElements.js @@ -32,7 +32,6 @@ export function readShellElements(document = globalThis.document) { _celestialBtn: document.getElementById('celestial-toggle'), _scopeBtn: document.getElementById('scope-toggle'), _scopeFeatherSlider: document.getElementById('scope-feather-slider'), - _scopeFeatherValue: document.getElementById('scope-feather-value'), _mapStackChips: document.getElementById('map-stack-chips'), _mapStackStatus: document.getElementById('map-stack-status'), _cleanViewBtn: document.getElementById('clean-view-toggle'), diff --git a/src/ui/views/panels/DisplayPanel.vue b/src/ui/views/panels/DisplayPanel.vue index 08dd6d9c3..008c84d33 100644 --- a/src/ui/views/panels/DisplayPanel.vue +++ b/src/ui/views/panels/DisplayPanel.vue @@ -5,9 +5,9 @@ import { t } from '../../../i18n/index.js'; import { vRangeValue } from '../../rail/rangeValue.js'; import { useDisplayStore } from '../../stores/display.js'; -// The Display panel. VisualSettings presents the HUD, Sonar, detection, -// bloom and sharpen and publishes them; until it does, the panel shows its -// initial markup. The other groups keep their owners' element bindings. +// The Display panel. VisualSettings presents the HUD, Sonar, Scope, +// Celestial, detection, bloom and sharpen and publishes them; until it does, +// the panel shows its initial markup. The other groups keep their owners' element bindings. const { view } = storeToRefs(useDisplayStore()); const pct = (value) => `${value}%`; const hud = computed(() => view.value?.hud ?? null); @@ -16,6 +16,14 @@ const sonarOn = computed(() => sonar.value?.enabled ?? true); const detection = computed(() => view.value?.detection ?? null); const detectButton = computed(() => detection.value?.button ?? null); const allocation = computed(() => detection.value?.allocation ?? 'ELASTIC'); +const scopeOn = computed(() => view.value?.scope.enabled ?? true); +const scopeFeather = computed(() => view.value?.scope.featherPct ?? 100); +const celestial = computed(() => view.value?.celestial ?? null); +const celestialTitle = computed(() => + celestial.value && !celestial.value.supported + ? t('Celestial ring — available in Normal style') + : t('Celestial ring — reveal the full globe'), +); const allocationSegment = (value) => ({ class: { active: allocation.value === value }, 'aria-checked': String(allocation.value === value), @@ -133,12 +141,12 @@ const allocationSegment = (value) => ({
Feather - - 100% + + {{ pct(scopeFeather) }}
@@ -177,7 +185,7 @@ const allocationSegment = (value) => ({
- diff --git a/src/ui/visualSettings.js b/src/ui/visualSettings.js index 2df7779e9..884225414 100644 --- a/src/ui/visualSettings.js +++ b/src/ui/visualSettings.js @@ -1,5 +1,4 @@ import { clampScopeFeatherPct } from '../scopeMaskPolicy.js'; -import { syncScopeControls } from './displayControls.js'; import { STYLE_STATUS_LABELS } from './visualPresets.js'; import { RETIRED_STYLES } from '../visualStylePolicy.js'; import { useDisplayStore } from './stores/display.js'; @@ -49,12 +48,7 @@ const DETECTION_ALLOCATION_STORAGE_KEY = 'gev:detection-allocation:v1'; /** Own visual preferences, detection overrides and display-control state. */ export class VisualSettings { async restoreShareState(state) { - const { - setScopeMaskEnabled, - setScopeMaskFeather, - setScopeTerminusOverride, - clampScopeTerminusPct, - } = this.services; + const { setScopeTerminusOverride, clampScopeTerminusPct } = this.services; const { style, bloom, @@ -139,17 +133,7 @@ export class VisualSettings { focus: false, }); } - if (typeof scopeEnabled === 'boolean') { - setScopeMaskEnabled(scopeEnabled); - syncScopeControls(this._scopeBtn, this._scopeFeatherSlider, scopeEnabled); - } - if (typeof scopeFeatherPct === 'number' && this._scopeFeatherSlider) { - const pct = clampScopeFeatherPct(scopeFeatherPct); - this._scopeFeatherSlider.value = String(pct); - if (this._scopeFeatherValue) - this._scopeFeatherValue.textContent = `${pct}%`; - setScopeMaskFeather(pct / 100); - } + this._setScope({ enabled: scopeEnabled, featherPct: scopeFeatherPct }); // null restores the altitude-adaptive ramp; a number pins the terminus // (clamped to the supported 94..100 band, same as the `sce` hash key). if (scopeTerminusPct === null) setScopeTerminusOverride(null); @@ -218,6 +202,10 @@ export class VisualSettings { this._hudVisibleShown = null; this._hudVariantShown = null; this._sonarShown = null; + // What the Scope and Celestial controls show; null until first set. + this._scopeEnabledShown = null; + this._scopeFeatherShown = null; + this._celestialShown = null; this.activeStyle = 'normal'; this._preCyberStyle = null; this._cyberStyleUserSelected = false; @@ -483,9 +471,31 @@ export class VisualSettings { detection: this._presentDetection(), hud: { visible: this._hudVisibleShown, variant: this._hudVariantShown }, sonar: this._sonarShown, + scope: { + enabled: this._scopeEnabledShown, + featherPct: this._scopeFeatherShown, + }, + celestial: this._celestialShown, }; } + /** + * Apply the Scope mask and show it on its controls; a field that is not + * given stays as it is. `apply: false` only shows the mask's state. + */ + _setScope({ enabled, featherPct } = {}, { apply = true } = {}) { + if (typeof enabled === 'boolean') { + if (apply) this.services.setScopeMaskEnabled(enabled); + this._scopeEnabledShown = enabled; + } + if (typeof featherPct === 'number') { + const pct = clampScopeFeatherPct(featherPct); + if (apply) this.services.setScopeMaskFeather(pct / 100); + this._scopeFeatherShown = pct; + } + this._publishDisplay(); + } + /** Detection's button, rows, sliders and allocation segments. */ _presentDetection() { const mode = this._detectionModeLabel; @@ -1150,15 +1160,8 @@ export class VisualSettings { if (syncShare) this.shareLinkManager?.claimRestoreLane?.('visual'); const nextEnabled = styleSupported && enabled; this.celestialRing?.setEnabled(nextEnabled); - this._celestialBtn?.classList.toggle('active', nextEnabled); - this._celestialBtn?.setAttribute('aria-pressed', String(nextEnabled)); - if (this._celestialBtn) { - this._celestialBtn.disabled = !styleSupported; - this._celestialBtn.setAttribute('aria-disabled', String(!styleSupported)); - this._celestialBtn.title = styleSupported - ? t('Celestial ring — reveal the full globe') - : t('Celestial ring — available in Normal style'); - } + this._celestialShown = { active: nextEnabled, supported: styleSupported }; + this._publishDisplay(); let cameraFocused = false; if (nextEnabled && focus) { cameraFocused = !!this.celestialRing?.focusFullGlobe(); @@ -1220,7 +1223,6 @@ export class VisualSettings { } async applyVisualState(state = {}, { isCurrent = null } = {}) { - const { setScopeMaskEnabled, setScopeMaskFeather } = this.services; const superseded = () => typeof isCurrent === 'function' && !isCurrent(); if (superseded()) return false; @@ -1258,21 +1260,10 @@ export class VisualSettings { } const scopeState = state.scope || {}; - if (typeof scopeState.enabled === 'boolean') { - setScopeMaskEnabled(scopeState.enabled); - syncScopeControls( - this._scopeBtn, - this._scopeFeatherSlider, - scopeState.enabled, - ); - } - if (typeof scopeState.featherPct === 'number' && this._scopeFeatherSlider) { - const pct = clampScopeFeatherPct(scopeState.featherPct); - this._scopeFeatherSlider.value = String(pct); - if (this._scopeFeatherValue) - this._scopeFeatherValue.textContent = `${pct}%`; - setScopeMaskFeather(pct / 100); - } + this._setScope({ + enabled: scopeState.enabled, + featherPct: scopeState.featherPct, + }); const detectionState = state.detection || {}; if (typeof detectionState.density === 'number')