Files
gods-eye-view/src/ui/visualSettings.js
T
Sameh Khamis 95523e09cd feat(panels): render each panel's state classes from the store
A panel's collapsed, dock-pinned, dock-pinned-top, layout-auto-collapsed
and accordion-collapsed classes are drawn by its root's v-panel-state from
the gev-panel-chrome store. PanelChrome, its restore, the layout
controller, both rails and the side accordion present each panel's state
there instead of writing its classes; the rails call onCollapse(panel,
{collapsed, auto}). The directive applies a change in a synchronous
watcher, since the rails measure a panel in the task that opens or closes
it.

Destroying the chrome inside Cockpit now puts back the panels Cockpit's
entry closed while it can still present them. Without a shell, the voice
tools' set_panel_open reports that there is no panel to open. QA scripts
present the states they set instead of writing the classes.
2026-10-31 23:51:47 -07:00

1595 lines
53 KiB
JavaScript

import { clampScopeFeatherPct } from '../scopeMaskPolicy.js';
import { STYLE_STATUS_LABELS } from './visualPresets.js';
import { RETIRED_STYLES } from '../visualStylePolicy.js';
import { useDisplayStore } from './stores/display.js';
import { applicationPinia } from './vueRuntime.js';
import { t } from '../i18n/index.js';
import { UiLifetime } from './uiLifetime.js';
import {
VisualEffects,
STYLES,
GLOBAL_POST_DEFAULTS,
STYLE_PRESET_DEFAULTS,
MILITARY_DETECTION_PRESET,
} from './effects.js';
import { createStyleParameters } from './visualInput.js';
import {
BLOOM_SCALE_VERSION,
clampBloomIntensity,
decodeBloomIntensity,
} from '../bloom.js';
import {
ALLOCATION_STRATEGIES,
canonicalizeDensity,
defaultDensityForProfile,
normalizeAllocationStrategy,
normalizeProfile,
profileForDensity,
} from '../data/detectionPolicy.js';
import {
applyCockpitVisionStageIntensities,
captureCockpitVisionBaseline,
normalizeCockpitVisionMode,
} from '../cockpitVisionPolicy.js';
import {
applyContactsDetection,
shareCacheNeedsHeal,
shareableDetectionState,
} from '../contactsDetectionPolicy.js';
import {
applyCyberSonarSettings,
isCyberSonarEnabled,
readCyberSonarSettings,
setCyberSonarEnabled,
} from '../cyberSonar.js';
import { cyberVisualDefaultsForHudTransition } from '../hudLayouts.js';
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 { setScopeTerminusOverride, clampScopeTerminusPct } = this.services;
const {
style,
bloom,
sharpen,
bloomIntensity,
bloomVersion,
sharpenIntensity,
hudVariant,
hudVisible,
detectionMode,
detectionDensity,
detectionAllocation,
detectionFadePct,
detectionOutsideOpacityPct,
celestialRing,
scopeEnabled,
scopeFeatherPct,
scopeTerminusPct,
mapStack,
panelState,
styleParams,
} = state || {};
// Ignore the retired 'ai-edit' style from older share links. Normal is
// the start-up style, but a share opened later in the tab must still leave
// a filtered style for it.
if (
style &&
style !== 'ai-edit' &&
(style !== 'normal' || this.activeStyle !== 'normal')
) {
const alreadyActive = style === this.activeStyle;
this.setStyle(style, {
applyPreset: true,
revealParameters: false,
restore: true,
});
// setStyle leaves an already-active style untouched; the incoming share
// still starts from that preset before its own parameters apply.
if (alreadyActive && style !== 'normal')
this._applyStylePresetDefaults(style);
}
if (styleParams && style && this.stages[style] && STYLES[style]?.uniforms) {
for (const [uniformName, uniformValue] of Object.entries(styleParams)) {
if (!Object.hasOwn(STYLES[style].uniforms, uniformName)) continue;
this.stages[style].uniforms[uniformName] = uniformValue;
}
this._updateSliderPanel(style, { reveal: false });
}
if (typeof bloomIntensity === 'number') {
const intensity = decodeBloomIntensity(bloomIntensity, bloomVersion);
this._setBloomIntensity(intensity, { syncShare: false });
}
if (typeof sharpenIntensity === 'number')
this._setSharpenIntensity(sharpenIntensity, { syncShare: false });
if (typeof bloom === 'boolean') this._setBloomEnabled(bloom);
if (typeof sharpen === 'boolean') this._setSharpenEnabled(sharpen);
if (hudVariant) this._setHudVariant(hudVariant);
if (typeof hudVisible === 'boolean') {
this.hud.setMode(hudVisible ? 'on' : 'off');
this._updateHudButtonState();
}
if (typeof detectionDensity === 'number')
this._setDetectionDensity(detectionDensity);
if (detectionAllocation) {
this._setDetectionAllocation(detectionAllocation, {
syncShare: false,
persist: false,
});
}
this._setDetectionFade({
fadePct:
typeof detectionFadePct === 'number' ? detectionFadePct : undefined,
outsideOpacityPct:
typeof detectionOutsideOpacityPct === 'number'
? detectionOutsideOpacityPct
: undefined,
});
if (detectionMode) this._setDetectionMode(detectionMode);
if (typeof celestialRing === 'boolean') {
this.setCelestialRingEnabled(celestialRing, {
syncShare: false,
focus: false,
});
}
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);
else if (typeof scopeTerminusPct === 'number') {
const pinned = clampScopeTerminusPct(scopeTerminusPct);
setScopeTerminusOverride(pinned == null ? null : pinned / 100);
}
const mapStackRestore = mapStack
? this._setMapStack(mapStack, { syncShare: false })
: Promise.resolve();
if (panelState) this._restorePanelState(panelState);
await mapStackRestore;
this._syncShareState();
}
constructor({
viewer,
services,
elements,
operations,
mapStackController,
readHud,
readCockpit,
readDataManager,
readShareLinks,
readCelestialRing,
readContextMode,
readContextChanging,
readDisplayPortalActive,
store = useDisplayStore(applicationPinia()),
}) {
this.store = store;
Object.assign(this, elements, operations, {
viewer,
services,
mapStackController,
readHud,
readCockpit,
readDataManager,
readShareLinks,
readCelestialRing,
readContextMode,
readContextChanging,
readDisplayPortalActive,
});
this._lifetime = new UiLifetime();
this._visualEffects = new VisualEffects({
viewer,
requestRender: services.governorRequestRender,
holdRender: services.holdContinuousRender,
releaseRender: services.releaseContinuousRender,
});
// The sharpen amount as a whole percentage. VisualEffects keeps the
// fraction it applies, and drops writes once it is stopped.
this._sharpenPct = 49;
// Detection's density stop, keyhole fade and outside opacity, in whole
// percentages; the mode label the button shows (null until first
// synced) and the allocation its segments show (null until set).
this._detectionDensityPct = 50;
this._detectionFadePct = 7;
this._detectionOutsidePct = 1;
this._detectionModeLabel = null;
this._detectionAllocationShown = null;
// What the HUD and Sonar controls show; null until first synced, so the
// initial markup stands.
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;
document.documentElement.dataset.gevStyle = this.activeStyle;
this._detectionUserOverridden = false;
this._cockpitVisionMode = 'optical';
this._cockpitVisionRestore = null;
this._contactsDetectionRestore = null;
let storedDetectionAllocation = 'ELASTIC';
try {
storedDetectionAllocation =
localStorage.getItem(DETECTION_ALLOCATION_STORAGE_KEY) || 'ELASTIC';
} catch {
/* unavailable storage */
}
this._detectionAllocationPreference = normalizeAllocationStrategy(
storedDetectionAllocation,
);
}
get hud() {
return this.readHud();
}
get cockpitView() {
return this.readCockpit();
}
get _dataManager() {
return this.readDataManager();
}
get shareLinkManager() {
return this.readShareLinks();
}
get celestialRing() {
return this.readCelestialRing();
}
get _contextMode() {
return this.readContextMode();
}
get _contextModeChanging() {
return this.readContextChanging();
}
get _cockpitDisplayPortalActive() {
return this.readDisplayPortalActive();
}
get stages() {
return this._visualEffects.stages;
}
get transitions() {
return this._visualEffects.transitions;
}
get bloomEnabled() {
return this._visualEffects.bloomEnabled;
}
get sharpenEnabled() {
return this._visualEffects.sharpenEnabled;
}
/** Bloom intensity as the Display slider shows it, 0-200. */
get bloomIntensityPct() {
return clampBloomIntensity(this._getBloomIntensity());
}
/** Sharpen amount as the Display slider shows it, 0-100. */
get sharpenIntensityPct() {
return this._sharpenPct;
}
_initStages() {
this._visualEffects.initStyles();
}
_setStageIntensity(stage, value) {
this._visualEffects.setStageIntensity(stage, value);
}
_syncStagesEnabledFromIntensity() {
this._visualEffects.syncStagesEnabledFromIntensity();
}
_syncContactsDetection() {
if (this._contextModeChanging) return;
const result = applyContactsDetection({
active: this._contextMode === 'flights',
restore: this._contactsDetectionRestore,
// A map style picked DURING the session owns detection on the way out —
// its auto-enable preset is younger than the entry snapshot.
styleOwnsDetection:
!this._detectionUserOverridden &&
Boolean(STYLE_PRESET_DEFAULTS[this.activeStyle]?.detection),
// The snapshot must cover everything activation mutates — the preset
// writes DENSITY as well as mode, so a mode-only snapshot returned
// OFF @ 25% as OFF @ 75% and the next manual enable came back Dense.
getState: () => {
const state = this.getDetectionState();
return { mode: state.detectionMode, densityPct: state.densityPct };
},
// Owner playtest: the force-on lands on the tactical look the military
// styles apply — the SAME preset object — not on whatever profile the
// operator last happened to leave detection at.
applyPreset: () => this._applyDetectionPreset(MILITARY_DETECTION_PRESET),
// The preset applier IS the state replayer: same density-then-mode order,
// same slider writes, so a restore round-trips exactly.
restoreState: (state) => this._applyDetectionPreset(state),
});
const hadOwnership = Boolean(this._contactsDetectionRestore);
this._contactsDetectionRestore = result.restore;
// Serialization reads that ownership: while Contacts holds it the link
// carries the SAVED snapshot, and once released it carries live state. The
// share cache therefore goes stale on any ownership transition, whether or
// not the detection engine itself moved — and it does not always move.
// Exiting while a military style owns detection returns changed:false (the
// style's preset already matches), and returning early there left a copied
// link claiming the operator's pre-Contacts values while the map showed
// Dense @ 75%.
if (
!shareCacheNeedsHeal({
changed: result.changed,
hadOwnership,
hasOwnership: Boolean(result.restore),
})
)
return;
if (result.changed) this._syncDetectionUiFromEngine();
this._syncShareState();
}
_setCockpitVision(mode, active, { revealParameters = false } = {}) {
const next = active ? normalizeCockpitVisionMode(mode) : 'optical';
if (!this.stages) return;
if (!active) {
if (this._cockpitVisionRestore) {
for (const [name, intensity] of Object.entries(
this._cockpitVisionRestore,
)) {
if (this.stages[name])
this._setStageIntensity(this.stages[name], intensity);
}
}
this._cockpitVisionRestore = null;
this._cockpitVisionMode = 'optical';
this._syncIrBoost(); // Cockpit exit: fall back to the map preset's IR state
this._updateSliderPanel(this.activeStyle, { reveal: false });
this._revealCockpitStyleParameters({ openDisplay: revealParameters });
return;
}
if (!this._cockpitVisionRestore) {
this._cockpitVisionRestore = captureCockpitVisionBaseline(
this.stages,
this.transitions,
);
}
if (next === 'optical') {
applyCockpitVisionStageIntensities(this.stages, next);
this._syncStagesEnabledFromIntensity();
this._cockpitVisionMode = next;
this._syncIrBoost();
this._updateSliderPanel(null, { reveal: false });
this._revealCockpitStyleParameters({ openDisplay: revealParameters });
return;
}
const target = applyCockpitVisionStageIntensities(this.stages, next);
this._syncStagesEnabledFromIntensity();
this._cockpitVisionMode = next;
this._syncIrBoost(); // Cockpit vision override ('nvg'/'thermal' boost; CRT/NOIR clear)
this._updateSliderPanel(target || null, { reveal: false });
this._revealCockpitStyleParameters({ openDisplay: revealParameters });
}
_syncIrBoost() {
const cockpitMode = this.cockpitView?.active
? this._cockpitVisionMode
: null;
const effective = cockpitMode
? cockpitMode === 'optical'
? 'normal'
: cockpitMode
: this.activeStyle;
const irBoost =
effective === 'surveillance' ||
effective === 'thermal' ||
effective === 'nvg';
this._dataManager?.setLayerParams('flights', { irBoost });
this._dataManager?.setLayerParams('military', { irBoost });
// Layers whose in-scene sprites restyle for a sensor preset need the
// EFFECTIVE style — a cockpit vision override sets no map style, so the
// map's own style event never fires for it.
window.dispatchEvent(
new CustomEvent('gev:vision-change', {
detail: {
style: effective,
cockpit: Boolean(cockpitMode),
},
}),
);
// Fog blends distant geometry toward an effectively-BLACK color in this
// app (the Cesium globe is hidden), so beyond ~100 km every 3D aircraft
// fogs to a black silhouette — lighting and shaders can't reach past it
// (owner cockpit-FLIR field rounds, 2026-08-16). IR sensors see through
// haze, so the boost styles simply turn fog off; the prior state restores
// on exit. Transition-guarded so repeated syncs don't clobber the saved value.
const scene = this.viewer?.scene;
if (scene?.fog && irBoost !== this._irBoostActive) {
this._irBoostActive = irBoost;
if (irBoost) {
this._irFogWasEnabled = scene.fog.enabled;
scene.fog.enabled = false;
} else if (this._irFogWasEnabled != null) {
scene.fog.enabled = this._irFogWasEnabled;
this._irFogWasEnabled = null;
}
scene.requestRender?.();
}
}
_syncCockpitInheritedStyle() {
if (!this.cockpitView?.active || !this.stages) return;
this._cockpitVisionRestore = Object.fromEntries(
Object.keys(this.stages).map((name) => [
name,
name === this.activeStyle ? 1 : 0,
]),
);
for (const name of Object.keys(this.stages)) this.transitions.delete(name);
this.cockpitView.setVisionMode(this.cockpitView.visionMode);
}
_revealCockpitStyleParameters({ openDisplay = false } = {}) {
if (
!this.cockpitView?.active ||
!this._sliderPanel?.classList.contains('active')
)
return;
if (openDisplay && !this._cockpitDisclosureOpen?.('display')) {
this._setCockpitDisclosure?.('display', true);
return;
}
if (!this._cockpitDisclosureOpen?.('display')) return;
this._presentPanel(this._sliderPanel, { collapsed: false });
this._lifetime.frame(() =>
this._lifetime.frame(() => {
this._sliderPanel?.scrollIntoView?.({ block: 'nearest' });
}),
);
}
_initBloomSharpen() {
this._visualEffects.initPostProcess(this._sharpenPct / 100);
this._publishDisplay();
}
/** Present the Display panel's post-processing groups. */
_presentDisplay() {
return {
bloom: { active: this.bloomEnabled, intensity: this.bloomIntensityPct },
sharpen: { active: this.sharpenEnabled, intensity: this._sharpenPct },
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;
const enabled = mode != null && mode !== 'OFF';
const labels = {
SPARSE: t('SPARSE'),
BALANCED: t('BALANCED'),
DENSE: t('DENSE'),
};
return {
// Null until the first sync, so the initial markup stands.
button:
mode == null
? null
: {
pressed: enabled,
ariaLabel: enabled
? t('Detection overlay: {mode}', {
mode: String(mode).toLowerCase(),
})
: t('Detection overlay: off'),
label: labels[mode] ?? t('DETECT'),
active: Object.hasOwn(labels, mode),
panoptic: mode === 'DENSE',
},
rowsVisible: enabled,
densityPct: this._detectionDensityPct,
fadePct: this._detectionFadePct,
outsideOpacityPct: this._detectionOutsidePct,
allocation: this._detectionAllocationShown,
};
}
_publishDisplay() {
this.store.publish(this._presentDisplay());
}
_getBloomIntensity() {
return this._visualEffects.bloomIntensity;
}
_syncBloomStageEnabled() {
this._visualEffects.syncBloomEnabled();
}
_setBloomIntensity(intensity, { syncShare = true } = {}) {
const { governorRequestRender } = this.services;
governorRequestRender('bloom');
this._applyBloomIntensity(clampBloomIntensity(intensity));
this._publishDisplay();
if (syncShare) this._syncShareState();
}
_applyBloomIntensity(intensity) {
this._visualEffects.applyBloomIntensity(intensity);
}
_setBloomEnabled(enabled) {
const { governorRequestRender } = this.services;
governorRequestRender('bloom');
this._visualEffects.setBloomEnabled(enabled);
this._syncBloomStageEnabled();
this._publishDisplay();
if (this.bloomEnabled) {
this._applyBloomIntensity(this._getBloomIntensity());
}
this._syncShareState();
this._layoutRightPanels();
}
_applySharpenIntensity(val) {
this._visualEffects.applySharpenIntensity(val);
}
/** Set the sharpen amount, a percentage clamped to 0-100, and apply it. */
_setSharpenIntensity(pct, { syncShare = true } = {}) {
if (!Number.isFinite(pct)) return;
this._sharpenPct = Math.max(0, Math.min(100, Math.round(pct)));
this._applySharpenIntensity(this._sharpenPct / 100);
this._publishDisplay();
if (syncShare) this._syncShareState();
}
_setSharpenEnabled(enabled) {
const { governorRequestRender } = this.services;
governorRequestRender('sharpen');
this._visualEffects.setSharpenEnabled(enabled);
this._publishDisplay();
if (this.sharpenEnabled)
this._applySharpenIntensity(this._sharpenPct / 100);
this._syncShareState();
this._layoutRightPanels();
}
/** Set detection's density to its nearest stop and apply it. */
_setDetectionDensity(pct = this._detectionDensityPct) {
const { getDetectionMode, setDetectionTuning } = this.services;
this._detectionDensityPct = canonicalizeDensity(pct);
setDetectionTuning({ densityPct: this._detectionDensityPct });
this._updateDetectionButton(getDetectionMode());
}
/**
* Set the keyhole fade distance (0-40%) and the opacity outside it
* (0-100%), keeping either one that is not given, and apply them.
*/
_setDetectionFade({
fadePct = this._detectionFadePct,
outsideOpacityPct = this._detectionOutsidePct,
} = {}) {
const { setKeyholeFadeTuning } = this.services;
const whole = (value, max) =>
Math.max(0, Math.min(max, Math.round(Number(value) || 0)));
this._detectionFadePct = whole(fadePct, 40);
this._detectionOutsidePct = whole(outsideOpacityPct, 100);
this._publishDisplay();
setKeyholeFadeTuning({
fadeRatio: this._detectionFadePct / 100,
outsideOpacity: this._detectionOutsidePct / 100,
});
this.viewer.scene.requestRender?.();
}
_setDetectionAllocation(strategy, { syncShare = true, persist = true } = {}) {
const { setDetectionTuning } = this.services;
const raw = String(strategy || '')
.trim()
.toUpperCase();
if (!ALLOCATION_STRATEGIES.includes(raw)) return false;
const normalized = normalizeAllocationStrategy(raw);
this._detectionAllocationPreference = normalized;
setDetectionTuning({ allocationStrategy: normalized });
this._detectionAllocationShown = normalized;
this._publishDisplay();
if (persist) {
try {
localStorage.setItem(DETECTION_ALLOCATION_STORAGE_KEY, normalized);
} catch {
/* best effort */
}
}
if (syncShare) this._syncShareState();
return true;
}
_syncDetectionUiFromEngine() {
const { getKeyholeFadeTuning, getDetectionTuning, getDetectionMode } =
this.services;
const tuning = getDetectionTuning();
this._detectionDensityPct = canonicalizeDensity(tuning.densityPct);
this._setDetectionAllocation(tuning.allocationStrategy, {
syncShare: false,
persist: false,
});
const fadeTuning = getKeyholeFadeTuning();
this._setDetectionFade({
fadePct: fadeTuning.fadeRatio * 100,
outsideOpacityPct: fadeTuning.outsideOpacity * 100,
});
this._updateDetectionButton(getDetectionMode());
}
_setDetectionMode(modeLabel) {
const { setDetectionModeByLabel } = this.services;
if (!modeLabel) return;
setDetectionModeByLabel(modeLabel);
this._syncDetectionUiFromEngine();
this._syncShareState();
}
_setHudVariant(variantName, { applyVisualDefaults = false } = {}) {
if (!variantName) return;
const previousVariant = this.hud.getVariant();
this.hud.setVariant(variantName);
const nextVariant = this.hud.getVariant();
const enteredCyber = previousVariant !== 'cyber' && nextVariant === 'cyber';
const leftCyber = previousVariant === 'cyber' && nextVariant !== 'cyber';
if (!applyVisualDefaults) {
// Scene/share/programmatic state is authoritative even when it reapplies
// the current Cyber variant. Do not let an older explicit entry restore
// stale local preset memory on the next user-selected exit.
this._preCyberStyle = null;
this._cyberStyleUserSelected = false;
} else if (enteredCyber) {
this._preCyberStyle = this.activeStyle;
this._cyberStyleUserSelected = false;
}
this._hudVariantShown = nextVariant;
this._publishDisplay();
const visualDefaults = cyberVisualDefaultsForHudTransition(
previousVariant,
nextVariant,
{ explicit: applyVisualDefaults },
);
if (visualDefaults) this._applyCyberVisualDefaults(visualDefaults);
if (leftCyber) {
const restoreStyle =
applyVisualDefaults && !this._cyberStyleUserSelected
? this._preCyberStyle
: null;
this._preCyberStyle = null;
this._cyberStyleUserSelected = false;
if (restoreStyle && restoreStyle !== this.activeStyle) {
this.setStyle(restoreStyle, {
applyPreset: false,
revealParameters: false,
preserveCyberRestore: true,
});
}
}
this._syncCyberSonarControl();
this._syncShareState();
this._scheduleAdaptivePanelLayout({ settle: true });
}
_applyCyberVisualDefaults({ style, ironbow }) {
this.setStyle(style, {
applyPreset: false,
revealParameters: false,
preserveCyberRestore: true,
});
const thermalUniforms = this.stages?.thermal?.uniforms;
if (!thermalUniforms || thermalUniforms.palette === undefined) return;
thermalUniforms.palette = ironbow;
this._updateSliderPanel('thermal', { reveal: false });
this.services.governorRequestRender('cyber-visual-defaults');
}
_syncCyberSonarControl() {
const settings = applyCyberSonarSettings(readCyberSonarSettings());
this._sonarShown = {
enabled: isCyberSonarEnabled(),
rings: settings.rings,
range: settings.range,
intensity: settings.intensity,
opacity: settings.opacity,
sector: settings.sector,
};
this._publishDisplay();
}
_setCyberSonarEnabled(enabled = !isCyberSonarEnabled()) {
const next = setCyberSonarEnabled(!!enabled);
this._syncCyberSonarControl();
this.services.governorRequestRender('cyber-sonar-toggle');
return next;
}
_setCyberSonarSetting(name, value) {
const settings = readCyberSonarSettings();
if (!Object.hasOwn(settings, name)) return settings;
const next = applyCyberSonarSettings({ ...settings, [name]: value });
this._syncCyberSonarControl();
this.services.governorRequestRender(`cyber-sonar-${name}`);
return next;
}
_applyStylePresetDefaults(styleName) {
const { governorRequestRender } = this.services;
const preset = STYLE_PRESET_DEFAULTS[styleName];
if (!preset) return;
if (preset.styleParams && typeof preset.styleParams === 'object') {
for (const [targetStyle, params] of Object.entries(preset.styleParams)) {
const stage = this.stages[targetStyle];
if (!stage || !params || typeof params !== 'object') continue;
for (const [uniformName, uniformValue] of Object.entries(params)) {
if (stage.uniforms[uniformName] === undefined) continue;
stage.uniforms[uniformName] = uniformValue;
governorRequestRender('style-param');
}
}
}
const bloomInput = preset.bloom || {};
if (typeof bloomInput.intensity === 'number') {
this._setBloomIntensity(clampBloomIntensity(bloomInput.intensity), {
syncShare: false,
});
}
if (typeof bloomInput.enabled === 'boolean') {
this._setBloomEnabled(bloomInput.enabled);
}
const sharpenInput = preset.sharpen || {};
if (typeof sharpenInput.intensity === 'number')
this._setSharpenIntensity(sharpenInput.intensity, { syncShare: false });
if (typeof sharpenInput.enabled === 'boolean') {
this._setSharpenEnabled(sharpenInput.enabled);
}
// Cyber is an explicit shell choice, independent of the imagery filter.
// Scene/share restoration still applies its own HUD through _setHudVariant.
if (preset.hudVariant && this.hud.getVariant() !== 'cyber') {
this._setHudVariant(preset.hudVariant);
}
if (typeof preset.hudVisible === 'boolean') {
this.hud.setMode(preset.hudVisible ? 'on' : 'off');
this._updateHudButtonState();
}
// A style may set a detection default (e.g. military styles -> Dense for
// the "epic" default view), but ONLY if the user hasn't manually changed
// detection this session. Detection is user-controlled and persists across
// style switches, so an explicit Sparse/Off choice is never stomped.
if (preset.detection && !this._detectionUserOverridden) {
this._applyDetectionPreset(preset.detection);
}
}
_applyDetectionPreset(det) {
if (!det) return;
if (typeof det.densityPct === 'number')
this._setDetectionDensity(det.densityPct);
if (det.mode) this._setDetectionMode(String(det.mode).toUpperCase());
}
_applyGlobalPostDefaults() {
const defaults = GLOBAL_POST_DEFAULTS;
if (typeof defaults.bloom?.intensity === 'number') {
this._setBloomIntensity(clampBloomIntensity(defaults.bloom.intensity), {
syncShare: false,
});
}
if (typeof defaults.bloom?.enabled === 'boolean') {
this._setBloomEnabled(defaults.bloom.enabled);
}
if (typeof defaults.sharpen?.intensity === 'number')
this._setSharpenIntensity(defaults.sharpen.intensity, {
syncShare: false,
});
if (typeof defaults.sharpen?.enabled === 'boolean') {
this._setSharpenEnabled(defaults.sharpen.enabled);
}
if (defaults.hudVariant) {
this._setHudVariant(defaults.hudVariant);
}
if (typeof defaults.hudVisible === 'boolean') {
this.hud.setMode(defaults.hudVisible ? 'on' : 'off');
this._updateHudButtonState();
}
if (defaults.detectionMode) {
this._setDetectionMode(defaults.detectionMode);
}
if (typeof defaults.detectionDensity === 'number')
this._setDetectionDensity(defaults.detectionDensity);
this._setDetectionAllocation(
this._detectionAllocationPreference ||
defaults.detectionAllocation ||
'ELASTIC',
{ syncShare: false, persist: false },
);
this._setDetectionFade({
fadePct: defaults.detectionFadePct ?? 7,
outsideOpacityPct: defaults.detectionOutsideOpacityPct ?? 1,
});
if (typeof defaults.celestialRing === 'boolean') {
this.setCelestialRingEnabled(defaults.celestialRing, {
syncShare: false,
focus: false,
});
}
}
_shareableDetectionState() {
const { getDetectionMode } = this.services;
return shareableDetectionState({
owned: this._contactsDetectionRestore,
liveMode: getDetectionMode(),
liveDensityPct: this._detectionDensityPct,
});
}
_readShareState() {
const {
getDetectionTuning,
isScopeMaskEnabled,
getScopeMaskFeather,
getScopeTerminusOverride,
} = this.services;
const detection = this._shareableDetectionState();
return {
bloomEnabled: this.bloomEnabled,
sharpenEnabled: this.sharpenEnabled,
options: {
bloomIntensity: this._getBloomIntensity(),
bloomVersion: BLOOM_SCALE_VERSION,
sharpenIntensity: this._sharpenPct,
hudVariant: this.hud.getVariant(),
hudVisible: this.hud.visible,
detectionMode: detection.mode,
detectionDensity: detection.densityPct,
detectionAllocation: getDetectionTuning().allocationStrategy,
detectionFadePct: this._detectionFadePct,
detectionOutsideOpacityPct: this._detectionOutsidePct,
celestialRingEnabled: this.celestialRingEnabled,
scopeEnabled: isScopeMaskEnabled(),
scopeFeatherPct: Math.round(getScopeMaskFeather() * 100),
// null when adaptive — the share layer omits `sce` entirely in that case.
scopeTerminusPct:
getScopeTerminusOverride() == null
? null
: Math.round(getScopeTerminusOverride() * 100),
mapStack: this.mapStackController?.getActiveId?.() || 'photoreal',
},
};
}
getDetectionState() {
const { getDetectionTuning, getDetectionMode } = this.services;
return {
detectionMode: getDetectionMode(),
densityPct: this._detectionDensityPct,
allocationStrategy: getDetectionTuning().allocationStrategy,
fadePct: this._detectionFadePct,
outsideOpacityPct: this._detectionOutsidePct,
};
}
getDetectionDiagnostics() {
const { readDetectionDiagnostics } = this.services;
return readDetectionDiagnostics();
}
setDetection({
enabled,
mode,
densityPct,
allocationStrategy,
fadePct,
outsideOpacityPct,
} = {}) {
const { getDetectionMode, setDetectionModeByLabel } = this.services;
if (enabled !== undefined && typeof enabled !== 'boolean') {
return {
ok: false,
error: `Invalid detection enabled value: ${enabled}`,
...this.getDetectionState(),
};
}
let requestedProfile = null;
if (typeof mode === 'string' && mode.trim()) {
requestedProfile = normalizeProfile(mode);
if (!requestedProfile) {
return {
ok: false,
error: `Unknown detection mode: ${mode}`,
...this.getDetectionState(),
};
}
}
let requestedDensity = null;
if (densityPct != null) {
if (!Number.isFinite(Number(densityPct))) {
return {
ok: false,
error: `Invalid density: ${densityPct}`,
...this.getDetectionState(),
};
}
requestedDensity = canonicalizeDensity(Number(densityPct));
}
if (
requestedProfile &&
requestedProfile !== 'OFF' &&
requestedDensity != null &&
profileForDensity(requestedDensity) !== requestedProfile
) {
return {
ok: false,
error: `Detection mode ${requestedProfile} conflicts with density ${requestedDensity}%`,
...this.getDetectionState(),
};
}
let requestedAllocation = null;
if (allocationStrategy != null) {
requestedAllocation = String(allocationStrategy).trim().toUpperCase();
if (!ALLOCATION_STRATEGIES.includes(requestedAllocation)) {
return {
ok: false,
error: `Unknown allocation strategy: ${allocationStrategy}`,
...this.getDetectionState(),
};
}
}
if (fadePct != null) {
if (!Number.isFinite(Number(fadePct))) {
return {
ok: false,
error: `Invalid fade distance: ${fadePct}`,
...this.getDetectionState(),
};
}
}
if (outsideOpacityPct != null) {
if (!Number.isFinite(Number(outsideOpacityPct))) {
return {
ok: false,
error: `Invalid outside opacity: ${outsideOpacityPct}`,
...this.getDetectionState(),
};
}
}
const hasExplicitVisualChange =
typeof enabled === 'boolean' ||
requestedProfile !== null ||
requestedDensity !== null ||
requestedAllocation !== null ||
fadePct != null ||
outsideOpacityPct != null;
if (hasExplicitVisualChange) {
// Voice/scripted detection control counts as an explicit user choice, so
// neither style presets nor a still-pending shared visual restore can
// overwrite it afterward.
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
}
if (requestedAllocation) {
this._setDetectionAllocation(requestedAllocation, { syncShare: false });
}
if (fadePct != null || outsideOpacityPct != null)
this._setDetectionFade({
fadePct: fadePct ?? undefined,
outsideOpacityPct: outsideOpacityPct ?? undefined,
});
if (
requestedProfile &&
requestedProfile !== 'OFF' &&
requestedDensity == null
) {
requestedDensity = defaultDensityForProfile(requestedProfile);
}
if (requestedDensity != null) this._setDetectionDensity(requestedDensity);
if (enabled === false || requestedProfile === 'OFF') {
setDetectionModeByLabel('OFF');
} else if (requestedProfile) {
setDetectionModeByLabel(requestedProfile);
} else if (enabled === true && getDetectionMode() === 'OFF') {
setDetectionModeByLabel(
profileForDensity(requestedDensity ?? this._detectionDensityPct),
);
}
this._syncDetectionUiFromEngine();
this._syncShareState();
return { ok: true, ...this.getDetectionState() };
}
setBloom({ enabled, intensityPct } = {}) {
const current = () => ({
enabled: !!this.bloomEnabled,
intensityPct: this.bloomIntensityPct,
});
if (enabled !== undefined && typeof enabled !== 'boolean') {
return {
ok: false,
error: `Invalid bloom enabled value: ${enabled}`,
bloom: current(),
};
}
if (
intensityPct !== undefined &&
(typeof intensityPct !== 'number' || !Number.isFinite(intensityPct))
) {
return {
ok: false,
error: `Invalid bloom intensity: ${intensityPct}`,
bloom: current(),
};
}
const hasExplicitVisualChange =
intensityPct !== undefined || enabled !== undefined;
if (hasExplicitVisualChange)
this.shareLinkManager?.claimRestoreLane?.('visual');
if (intensityPct !== undefined) {
this._setBloomIntensity(
Math.round(Math.max(0, Math.min(200, intensityPct))),
);
}
if (enabled !== undefined) this._setBloomEnabled(enabled);
return {
ok: true,
bloom: current(),
};
}
setSharpen({ enabled, intensityPct } = {}) {
const current = () => ({
enabled: !!this.sharpenEnabled,
intensityPct: this.sharpenIntensityPct,
});
if (enabled !== undefined && typeof enabled !== 'boolean') {
return {
ok: false,
error: `Invalid sharpen enabled value: ${enabled}`,
sharpen: current(),
};
}
if (
intensityPct !== undefined &&
(typeof intensityPct !== 'number' || !Number.isFinite(intensityPct))
) {
return {
ok: false,
error: `Invalid sharpen intensity: ${intensityPct}`,
sharpen: current(),
};
}
const hasExplicitVisualChange =
intensityPct !== undefined || enabled !== undefined;
if (hasExplicitVisualChange)
this.shareLinkManager?.claimRestoreLane?.('visual');
if (intensityPct !== undefined) {
this._setSharpenIntensity(intensityPct);
}
if (enabled !== undefined) this._setSharpenEnabled(enabled);
return {
ok: true,
sharpen: current(),
};
}
get celestialRingEnabled() {
return !!this.celestialRing?.enabled;
}
setCelestialRingEnabled(enabled, { syncShare = true, focus = false } = {}) {
const { isCelestialRingStyleSupported } = this.services;
const styleSupported = isCelestialRingStyleSupported(this.activeStyle);
const current = () => ({
enabled: this.celestialRingEnabled,
visible: !!this.celestialRing?.visible,
});
if (typeof enabled !== 'boolean') {
return {
ok: false,
celestialRing: current(),
cameraFocused: false,
error: `Invalid celestial ring enabled value: ${enabled}`,
};
}
if (typeof syncShare !== 'boolean' || typeof focus !== 'boolean') {
return {
ok: false,
celestialRing: current(),
cameraFocused: false,
error: 'Celestial ring options must be boolean',
};
}
if (!styleSupported && enabled) {
return {
ok: false,
celestialRing: current(),
cameraFocused: false,
error: 'Celestial ring is available only in Normal style',
};
}
if (syncShare) this.shareLinkManager?.claimRestoreLane?.('visual');
const nextEnabled = styleSupported && enabled;
this.celestialRing?.setEnabled(nextEnabled);
this._celestialShown = { active: nextEnabled, supported: styleSupported };
this._publishDisplay();
let cameraFocused = false;
if (nextEnabled && focus) {
cameraFocused = !!this.celestialRing?.focusFullGlobe();
}
if (syncShare) this._syncShareState();
return {
ok: styleSupported || !enabled,
celestialRing: current(),
cameraFocused,
};
}
getVisualState() {
const {
getDetectionTuning,
getDetectionMode,
isScopeMaskEnabled,
getScopeMaskFeather,
} = this.services;
const styleParams = {};
for (const [styleName, stage] of Object.entries(this.stages)) {
const shader = STYLES[styleName];
if (!shader?.uniforms) continue;
styleParams[styleName] = {};
for (const uniformName of Object.keys(shader.uniforms)) {
styleParams[styleName][uniformName] = stage.uniforms[uniformName];
}
}
return {
style: this.activeStyle,
bloom: {
enabled: this.bloomEnabled,
intensity: this._getBloomIntensity(),
version: BLOOM_SCALE_VERSION,
},
sharpen: {
enabled: this.sharpenEnabled,
intensity: this._sharpenPct,
},
hud: {
visible: this.hud.visible,
variant: this.hud.getVariant(),
},
detection: {
mode: getDetectionMode(),
density: this._detectionDensityPct,
allocation: getDetectionTuning().allocationStrategy,
fadePct: this._detectionFadePct,
outsideOpacityPct: this._detectionOutsidePct,
},
scope: {
enabled: isScopeMaskEnabled(),
featherPct: Math.round(getScopeMaskFeather() * 100),
},
mapStack: this.mapStackController?.getActiveId?.() || 'photoreal',
styleParams,
};
}
async applyVisualState(state = {}, { isCurrent = null } = {}) {
const superseded = () => typeof isCurrent === 'function' && !isCurrent();
if (superseded()) return false;
const style = RETIRED_STYLES.includes(state.style) ? 'normal' : state.style;
if (style && style !== this.activeStyle) {
this.setStyle(style, { applyPreset: false });
}
const bloomState = state.bloom || {};
if (typeof bloomState.intensity === 'number') {
const intensity = decodeBloomIntensity(
bloomState.intensity,
bloomState.version ?? state.bloomVersion ?? BLOOM_SCALE_VERSION,
);
this._setBloomIntensity(intensity, { syncShare: false });
}
if (typeof bloomState.enabled === 'boolean') {
this._setBloomEnabled(bloomState.enabled);
}
const sharpenState = state.sharpen || {};
if (typeof sharpenState.intensity === 'number')
this._setSharpenIntensity(sharpenState.intensity, { syncShare: false });
if (typeof sharpenState.enabled === 'boolean') {
this._setSharpenEnabled(sharpenState.enabled);
}
const hudState = state.hud || {};
if (hudState.variant) {
this._setHudVariant(hudState.variant);
}
if (typeof hudState.visible === 'boolean') {
this.hud.setMode(hudState.visible ? 'on' : 'off');
this._updateHudButtonState();
}
const scopeState = state.scope || {};
this._setScope({
enabled: scopeState.enabled,
featherPct: scopeState.featherPct,
});
const detectionState = state.detection || {};
if (typeof detectionState.density === 'number')
this._setDetectionDensity(detectionState.density);
if (detectionState.allocation) {
this._setDetectionAllocation(detectionState.allocation, {
syncShare: false,
});
}
this._setDetectionFade({
fadePct:
typeof detectionState.fadePct === 'number'
? detectionState.fadePct
: undefined,
outsideOpacityPct:
typeof detectionState.outsideOpacityPct === 'number'
? detectionState.outsideOpacityPct
: undefined,
});
if (detectionState.mode) {
this._setDetectionMode(detectionState.mode);
}
if (state.mapStack) {
// The stack switch is itself a MUTATION, not merely a suspension point,
// so it needs a gate on BOTH sides of the await.
if (superseded()) return false;
const stackBefore = this.mapStackController?.getActiveId?.() ?? null;
const genBefore =
this.mapStackController?.getSwitchGeneration?.() ?? null;
await this._setMapStack(state.mapStack, { syncShare: false });
if (superseded()) {
// Superseded DURING the switch, which the pre-check above cannot catch
// and which has already moved the globe. The controller only
// invalidates a switch when another setStack() arrives, and a winning
// state that omits `mapStack` never issues one — every normalized scene
// shot omits it — so this stale globe would simply stand. Put back what
// the winner inherited.
const genAfter =
this.mapStackController?.getSwitchGeneration?.() ?? null;
// _setMapStack issues exactly one setStack(), which advances the
// generation once, or not at all when the stack was unavailable and
// nothing was mutated. Anything past that is a NEWER switch whose
// caller owns the globe now, and reverting would stomp a live intent.
const globeIsStillOurs =
genBefore !== null && genAfter !== null && genAfter <= genBefore + 1;
const landed = this.mapStackController?.getActiveId?.() ?? null;
if (globeIsStillOurs && stackBefore && landed !== stackBefore) {
await this._setMapStack(stackBefore, { syncShare: false });
}
return false;
}
// Everything below is the uniform commit, already past its own gate.
}
if (state.styleParams && typeof state.styleParams === 'object') {
for (const [styleName, params] of Object.entries(state.styleParams)) {
const stage = this.stages[styleName];
if (!stage || !params) continue;
for (const [uniformName, uniformValue] of Object.entries(params)) {
if (stage.uniforms[uniformName] === undefined) continue;
stage.uniforms[uniformName] = uniformValue;
}
}
this._updateSliderPanel(this.activeStyle);
}
this._syncShareState();
return true;
}
applyCinematicPreset(preset = {}) {
const bloomInput =
typeof preset.bloom === 'object'
? preset.bloom
: { intensity: preset.bloom };
let decodedBloomIntensity = null;
if (typeof bloomInput.intensity === 'number') {
decodedBloomIntensity = decodeBloomIntensity(
bloomInput.intensity,
bloomInput.version ?? preset.bloomVersion ?? BLOOM_SCALE_VERSION,
);
this._setBloomIntensity(decodedBloomIntensity, { syncShare: false });
}
if (typeof bloomInput.enabled === 'boolean') {
this._setBloomEnabled(bloomInput.enabled);
} else if (typeof bloomInput.intensity === 'number') {
this._setBloomEnabled(
(decodedBloomIntensity ?? this._getBloomIntensity()) > 0,
);
}
const sharpenInput =
typeof preset.sharpen === 'object'
? preset.sharpen
: { enabled: preset.sharpen };
if (typeof sharpenInput.intensity === 'number')
this._setSharpenIntensity(sharpenInput.intensity, { syncShare: false });
if (typeof sharpenInput.enabled === 'boolean') {
this._setSharpenEnabled(sharpenInput.enabled);
} else if (typeof sharpenInput.intensity === 'number') {
this._setSharpenEnabled(sharpenInput.intensity > 0);
}
if (preset.hudVariant) {
this._setHudVariant(preset.hudVariant);
}
if (preset.detectionMode) {
this._setDetectionMode(preset.detectionMode);
}
if (typeof preset.detectionDensity === 'number')
this._setDetectionDensity(preset.detectionDensity);
if (preset.detectionAllocation) {
this._setDetectionAllocation(preset.detectionAllocation, {
syncShare: false,
});
}
if (preset.styleParams && typeof preset.styleParams === 'object') {
for (const [styleName, params] of Object.entries(preset.styleParams)) {
const stage = this.stages[styleName];
if (!stage || !params || typeof params !== 'object') continue;
for (const [uniformName, uniformValue] of Object.entries(params)) {
if (stage.uniforms[uniformName] === undefined) continue;
stage.uniforms[uniformName] = uniformValue;
}
}
// Keep slider panel values in sync when updating the active style.
this._updateSliderPanel(this.activeStyle);
}
this._syncShareState();
}
_updateSliderPanel(styleName, { reveal = false } = {}) {
const { governorRequestRender } = this.services;
this._styleParameters ||= createStyleParameters({
container: this._sliderContainer,
});
this._styleParameters.clear();
const shader = STYLES[styleName];
if (!shader || !shader.uniforms || styleName === 'normal') {
this._sliderPanel.classList.remove('active');
this._scheduleRightPanelLayout();
return;
}
this._styleParameters.render({
uniforms: shader.uniforms,
readValue: (uName) => this.stages[styleName].uniforms[uName],
writeValue: (uName, val) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this.stages[styleName].uniforms[uName] = val;
},
onChange: () => {
// Uniform writes need an explicit render under the idle governor.
governorRequestRender('style-param-slider');
this._syncShareState();
},
});
this._sliderPanel.classList.add('active');
this._scheduleRightPanelLayout();
if (reveal) this._revealStyleParameters();
}
_revealStyleParameters() {
if (!this._sliderPanel?.classList.contains('active')) return;
if (this._cockpitDisplayPortalActive) return;
this._presentPanel(this._sliderPanel, { collapsed: false });
this.setPanelCollapsed('pp-toggles', false, { explicit: true });
this._lifetime.frame(() =>
this._lifetime.frame(() => {
const scrollOwner = this._ppToggles;
if (!scrollOwner) return;
const ownerRect = scrollOwner.getBoundingClientRect();
const panelRect = this._sliderPanel.getBoundingClientRect();
scrollOwner.scrollTop += panelRect.top - ownerRect.top - 8;
}),
);
}
setStyle(
styleName,
{
applyPreset = true,
revealParameters = applyPreset,
restore = false,
userInitiated = false,
preserveCyberRestore = false,
} = {},
) {
if (
typeof styleName !== 'string' ||
(styleName !== 'normal' && !Object.hasOwn(STYLES, styleName))
)
return;
const { setDetectionStyle } = this.services;
if (!restore) this.shareLinkManager?.claimRestoreLane?.('visual');
if (this.hud.getVariant() === 'cyber') {
if (userInitiated) {
// A direct preset choice owns the active style even when it selects the
// same value as Cyber's automatic FLIR default.
this._cyberStyleUserSelected = true;
} else if (!preserveCyberRestore) {
// Scene/share/reset state is authoritative and must not be overwritten
// by a remembered local Cyber entry when the user later exits.
this._preCyberStyle = null;
this._cyberStyleUserSelected = false;
}
}
if (styleName === this.activeStyle) {
if (revealParameters && styleName !== 'normal')
this._revealStyleParameters();
return;
}
const previousStyle = this.activeStyle;
this.activeStyle = styleName;
document.documentElement.dataset.gevStyle = styleName;
// The celestial optics treatment belongs to the unfiltered globe only.
// Leaving Normal turns it off; returning merely re-enables the control.
this.setCelestialRingEnabled(false, { syncShare: false, focus: false });
// Transition out the previous shader style
if (previousStyle !== 'normal' && this.stages[previousStyle]) {
this._startTransition(
previousStyle,
this.stages[previousStyle].uniforms.intensity,
0.0,
);
}
// Transition in the new shader style
if (styleName !== 'normal' && this.stages[styleName]) {
this._startTransition(
styleName,
this.stages[styleName].uniforms.intensity,
1.0,
);
}
if (applyPreset) {
this._applyStylePresetDefaults(styleName);
}
// The style buttons, the dock's mini status and the ACTIVE STYLE corner.
this._publishActiveStyle(styleName);
// Update parameter sliders
this._updateSliderPanel(styleName, { reveal: revealParameters });
// Notify HUD (color adaptation + auto show/hide)
this.hud.onStyleChange(styleName);
this._updateHudButtonState();
// Sync detection overlay tone to active post-process style
setDetectionStyle(styleName);
this._syncIrBoost();
window.dispatchEvent(
new CustomEvent('gev:style-change', {
detail: { style: styleName },
}),
);
this._syncCockpitInheritedStyle();
// Notify share link manager
this.shareLinkManager.onStyleChange(styleName);
this._syncShareState();
}
_startTransition(styleName, fromValue, toValue) {
this._visualEffects.startTransition(styleName, fromValue, toValue);
}
/** Publish the active style and its label in the UI language. */
_publishActiveStyle(styleName = this.activeStyle) {
const name = String(styleName || 'normal');
this.store.publishStyle({
name,
label: STYLE_STATUS_LABELS[name]
? t(STYLE_STATUS_LABELS[name])
: name.toUpperCase(),
});
}
_updateHudButtonState() {
this._syncHudControls();
}
/**
* Show the HUD's current visibility and layout on its controls; the layout
* row's visibility changes the panel's height, so the rails settle again.
*/
_syncHudControls() {
this._hudVisibleShown = Boolean(this.hud.visible);
this._hudVariantShown = this.hud.getVariant();
this._publishDisplay();
this._scheduleAdaptivePanelLayout({ settle: true });
}
_updateDetectionButton(modeLabel) {
this._detectionModeLabel = modeLabel;
this._publishDisplay();
this._layoutRightPanels();
}
stop() {
this._lifetime.destroy();
this._styleParameters?.destroy();
this._visualEffects.stop();
}
releaseIrBoost() {
if (!this._irBoostActive) return;
if (this._irFogWasEnabled != null && this.viewer?.scene?.fog)
this.viewer.scene.fog.enabled = this._irFogWasEnabled;
this._dataManager?.setLayerParams('flights', { irBoost: false });
this._dataManager?.setLayerParams('military', { irBoost: false });
this._irBoostActive = false;
this._irFogWasEnabled = null;
}
destroy() {
this.stop();
this.releaseIrBoost();
this._visualEffects.destroy();
}
}