feat(ui): render the Display panel's Scope and Celestial controls from the store

This commit is contained in:
Sameh Khamis committed 2026-10-30 23:07:21 -07:00
1 parent 3aa5a3e5e3
commit 1139d364c7
10 files changed
+110 -99

No files matched your search

+3 -3
View File
@@ -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.
+2 -2
View File
@@ -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'],
+1 -7
View File
@@ -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),
+6 -11
View File
@@ -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) => {
+5 -19
View File
@@ -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) => {
-6
View File
@@ -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;
}
+44
View File
@@ -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');
});
-1
View File
@@ -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'),
+15 -7
View File
@@ -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) => ({
<label class="pp-toggle-btn pp-scope-toggle" title="Scope — the circular viewport mask" data-i18n-title>
<span class="pp-icon material-symbols-outlined" aria-hidden="true">adjust</span>
<span class="pp-label" data-i18n>Scope</span>
<input type="checkbox" id="scope-toggle" checked />
<input type="checkbox" id="scope-toggle" :checked="scopeOn" />
</label>
<div class="pp-slider-row visible" id="scope-slider-row">
<span class="pp-slider-mini-label" data-i18n>Feather</span>
<input type="range" class="gev-quantitative-slider pp-slider" id="scope-feather-slider" min="0" max="200" value="100" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="scope-feather-value">100%</span>
<input type="range" class="gev-quantitative-slider pp-slider" id="scope-feather-slider" min="0" max="200" value="100" v-range-value="scopeFeather" :disabled="!scopeOn" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="scope-feather-value">{{ pct(scopeFeather) }}</span>
</div>
</div>
<div class="pp-toggle-group">
@@ -177,7 +185,7 @@ const allocationSegment = (value) => ({
<span class="draw-hint geo-import-hint" id="geo-import-hint" aria-live="polite"></span>
</div>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="celestial-toggle" title="Celestial ring — reveal the full globe" aria-pressed="false" data-i18n-title>
<button class="pp-toggle-btn" :class="{ active: celestial?.active }" id="celestial-toggle" :title="celestialTitle" :aria-pressed="String(celestial?.active ?? false)" :disabled="celestial ? !celestial.supported : undefined" :aria-disabled="celestial ? String(!celestial.supported) : undefined">
<span class="pp-icon material-symbols-outlined" aria-hidden="true">flare</span>
<span class="pp-label" data-i18n>Celestial</span>
</button>
+34 -43
View File
@@ -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')