fix(ui): preserve Cyber and Cockpit visual state

This commit is contained in:
Manjunath committed 2026-10-01 17:00:10 +05:30
1 parent e03bf008f3
commit 1dbc8a4452
16 files changed
+361 -97

No files matched your search

+33 -9
View File
@@ -86,23 +86,47 @@ test('Cockpit heading tape leaves the bottom exit row unobstructed', () => {
assert.doesNotMatch(css, /cockpit-compass-label/);
});
test('Cockpit vision cycle exposes exactly five real visual styles without NONE', () => {
assert.match(cycleVisionMode.toString(), /const modes = COCKPIT_VISION_MODES;/);
assert.match(setVisionMode.toString(), /const labels = \{\s*optical: inherited,\s*crt: 'CRT',\s*nvg: 'NVG',\s*thermal: 'FLIR',\s*noir: 'NOIR',?\s*\};/);
test('Cockpit vision cycle exposes every map preset exactly once', () => {
assert.match(
cycleVisionMode.toString(),
/const modes = COCKPIT_VISION_MODES;/,
);
assert.match(
setVisionMode.toString(),
/const labels = \{\s*optical: 'NORMAL',\s*crt: 'CRT',\s*nvg: 'NVG',\s*thermal: 'FLIR',\s*anime: 'ANIME',\s*noir: 'NOIR',\s*snow: 'SNOW',?\s*\};/,
);
assert.doesNotMatch(setVisionMode.toString(), /none: 'NONE'/);
assert.match(ui, /getInheritedVisionLabel: \(\) =>\s*\(?[\s\S]*?STYLE_STATUS_LABELS\[this\.activeStyle\]/);
assert.doesNotMatch(ui, /getInheritedVisionLabel/);
assert.match(html, /id="cockpit-vision-current-label"[^>]*>NORMAL<\/strong>/);
assert.match(ui, /const target = applyCockpitVisionStageIntensities\(\s*this\.stages,\s*next,\s*this\._cockpitVisionRestore,?\s*\);/);
assert.match(ui, /this\._cockpitVisionRestore = captureCockpitVisionBaseline\(\s*this\.stages,\s*this\.transitions,?\s*\);/);
assert.match(
cockpitEnter.toString(),
/this\.setVisionMode\(this\.getInitialVisionMode\(\)\)/,
'Cockpit must enter on the mode matching the active map preset',
);
assert.match(
readRadioSource(
new URL('./ui/cockpitCoordinator.js', import.meta.url),
'utf8',
),
/getInitialVisionMode:\s*\(\)\s*=>\s*cockpitVisionModeForStyle\(this\.activeStyle\)/,
);
assert.match(
ui,
/if \(next === 'optical'\) \{[\s\S]*?applyCockpitVisionStageIntensities\(\s*this\.stages,\s*next,\s*this\._cockpitVisionRestore,?\s*\);[\s\S]*?return;[\s\S]*?const target = applyCockpitVisionStageIntensities/,
'the inherited entry must restore the map shader while CRT, NVG, FLIR, and NOIR remain temporary Cockpit overrides',
/const target = applyCockpitVisionStageIntensities\(\s*this\.stages,\s*next,?\s*\);/,
);
assert.match(
ui,
/this\._cockpitVisionRestore = captureCockpitVisionBaseline\(\s*this\.stages,\s*this\.transitions,?\s*\);/,
);
assert.match(
ui,
/if \(next === 'optical'\) \{[\s\S]*?applyCockpitVisionStageIntensities\(\s*this\.stages,\s*next,?\s*\);[\s\S]*?this\._updateSliderPanel\(null,[\s\S]*?return;[\s\S]*?const target = applyCockpitVisionStageIntensities/,
'Normal must clear the inherited map shader while the other map presets remain temporary Cockpit overrides',
);
assert.match(
ui,
/_syncCockpitInheritedStyle\(\)[\s\S]*?name === this\.activeStyle \? 1 : 0[\s\S]*?this\.transitions\.delete\(name\)[\s\S]*?setVisionMode\(this\.cockpitView\.visionMode\)/,
'changing the map preset in Cockpit must refresh both the inherited label and restore baseline',
'changing the map preset in Cockpit must refresh the exit restore baseline without replacing Normal',
);
assert.match(ui, /setStyle\([\s\S]*?this\._syncCockpitInheritedStyle\(\);/);
});
+22 -5
View File
@@ -3,21 +3,40 @@ export const COCKPIT_VISION_MODES = Object.freeze([
'crt',
'nvg',
'thermal',
'anime',
'noir',
'snow',
]);
const TARGET_STYLE_BY_MODE = Object.freeze({
crt: 'retro',
nvg: 'surveillance',
thermal: 'thermal',
anime: 'anime',
noir: 'noir',
snow: 'snow',
});
/** Normalize a requested Cockpit vision mode to the inherited preset entry. */
const COCKPIT_MODE_BY_STYLE = Object.freeze({
normal: 'optical',
retro: 'crt',
surveillance: 'nvg',
thermal: 'thermal',
anime: 'anime',
noir: 'noir',
snow: 'snow',
});
/** Normalize a requested Cockpit vision mode to the unfiltered Normal entry. */
export function normalizeCockpitVisionMode(mode) {
return COCKPIT_VISION_MODES.includes(mode) ? mode : 'optical';
}
/** Start Cockpit on the equivalent supported map preset. */
export function cockpitVisionModeForStyle(style) {
return COCKPIT_MODE_BY_STYLE[String(style || '').toLowerCase()] || 'optical';
}
/** Settle pending map-style crossfades and return their intended final intensities. */
export function captureCockpitVisionBaseline(stages, transitions) {
const baseline = {};
@@ -34,12 +53,10 @@ export function captureCockpitVisionBaseline(stages, transitions) {
* Apply Cockpit-only stage intensities without changing any shader parameters.
* Returns the temporary style whose parameters should be shown, or null.
*/
export function applyCockpitVisionStageIntensities(stages, mode, restore = {}) {
export function applyCockpitVisionStageIntensities(stages, mode) {
const next = normalizeCockpitVisionMode(mode);
if (next === 'optical') {
for (const [name, intensity] of Object.entries(restore)) {
if (stages[name]) stages[name].uniforms.intensity = intensity;
}
for (const stage of Object.values(stages)) stage.uniforms.intensity = 0;
return null;
}
+75 -23
View File
@@ -3,6 +3,7 @@ import test from 'node:test';
import {
applyCockpitVisionStageIntensities,
captureCockpitVisionBaseline,
cockpitVisionModeForStyle,
COCKPIT_VISION_MODES,
normalizeCockpitVisionMode,
} from './cockpitVisionPolicy.js';
@@ -14,12 +15,31 @@ const createStages = () => ({
thermal: { uniforms: { intensity: 0, heat: 0.8 } },
});
test('Cockpit vision order exposes inherited, CRT, NVG, FLIR, and NOIR modes', () => {
assert.deepEqual(COCKPIT_VISION_MODES, ['optical', 'crt', 'nvg', 'thermal', 'noir']);
test('Cockpit vision order mirrors the unique map preset order', () => {
assert.deepEqual(COCKPIT_VISION_MODES, [
'optical',
'crt',
'nvg',
'thermal',
'anime',
'noir',
'snow',
]);
assert.equal(normalizeCockpitVisionMode('none'), 'optical');
assert.equal(normalizeCockpitVisionMode('unknown'), 'optical');
});
test('Cockpit entry selects the vision mode equivalent to the active map style', () => {
assert.equal(cockpitVisionModeForStyle('normal'), 'optical');
assert.equal(cockpitVisionModeForStyle('retro'), 'crt');
assert.equal(cockpitVisionModeForStyle('surveillance'), 'nvg');
assert.equal(cockpitVisionModeForStyle('thermal'), 'thermal');
assert.equal(cockpitVisionModeForStyle('anime'), 'anime');
assert.equal(cockpitVisionModeForStyle('noir'), 'noir');
assert.equal(cockpitVisionModeForStyle('snow'), 'snow');
assert.equal(cockpitVisionModeForStyle('unsupported'), 'optical');
});
test('Cockpit settles pending map crossfades before a temporary preset takes ownership', () => {
const stages = createStages();
const transitions = new Map([
@@ -29,49 +49,81 @@ test('Cockpit settles pending map crossfades before a temporary preset takes own
const restore = captureCockpitVisionBaseline(stages, transitions);
assert.deepEqual(restore, { noir: 1, retro: 0, surveillance: 0, thermal: 0 });
assert.equal(transitions.size, 0);
applyCockpitVisionStageIntensities(stages, 'nvg', restore);
applyCockpitVisionStageIntensities(stages, 'nvg');
assert.equal(stages.surveillance.uniforms.intensity, 1);
applyCockpitVisionStageIntensities(stages, 'optical', restore);
assert.equal(stages.noir.uniforms.intensity, 1);
applyCockpitVisionStageIntensities(stages, 'optical');
assert.equal(stages.noir.uniforms.intensity, 0);
assert.equal(stages.retro.uniforms.intensity, 0);
});
test('returning from a temporary preset restores the exact inherited intensities', () => {
test('Normal clears every temporary and inherited visual stage', () => {
const stages = createStages();
const restore = Object.fromEntries(
Object.entries(stages).map(([name, stage]) => [name, stage.uniforms.intensity]),
Object.entries(stages).map(([name, stage]) => [
name,
stage.uniforms.intensity,
]),
);
applyCockpitVisionStageIntensities(stages, 'thermal', restore);
applyCockpitVisionStageIntensities(stages, 'optical', restore);
applyCockpitVisionStageIntensities(stages, 'thermal');
applyCockpitVisionStageIntensities(stages, 'optical');
assert.deepEqual(
Object.fromEntries(Object.entries(stages).map(([name, stage]) => [name, stage.uniforms.intensity])),
restore,
Object.fromEntries(
Object.entries(stages).map(([name, stage]) => [
name,
stage.uniforms.intensity,
]),
),
{ noir: 0, retro: 0, surveillance: 0, thermal: 0 },
);
assert.deepEqual(restore, {
noir: 0.72,
retro: 0.18,
surveillance: 0,
thermal: 0,
});
});
test('temporary styles replace each other without changing the inherited restore snapshot', () => {
const stages = createStages();
const restore = Object.fromEntries(
Object.entries(stages).map(([name, stage]) => [name, stage.uniforms.intensity]),
Object.entries(stages).map(([name, stage]) => [
name,
stage.uniforms.intensity,
]),
);
applyCockpitVisionStageIntensities(stages, 'thermal', restore);
assert.equal(applyCockpitVisionStageIntensities(stages, 'crt', restore), 'retro');
applyCockpitVisionStageIntensities(stages, 'thermal');
assert.equal(applyCockpitVisionStageIntensities(stages, 'crt'), 'retro');
assert.equal(stages.retro.uniforms.intensity, 1);
assert.equal(stages.noir.uniforms.intensity, 0);
applyCockpitVisionStageIntensities(stages, 'optical', restore);
assert.equal(stages.noir.uniforms.intensity, 0.72);
assert.equal(stages.retro.uniforms.intensity, 0.18);
applyCockpitVisionStageIntensities(stages, 'optical');
assert.equal(stages.noir.uniforms.intensity, 0);
assert.equal(stages.retro.uniforms.intensity, 0);
assert.deepEqual(restore, {
noir: 0.72,
retro: 0.18,
surveillance: 0,
thermal: 0,
});
});
test('NOIR is a temporary Cockpit override and inherited restores the captured map style', () => {
test('NOIR is a temporary Cockpit override and Normal clears it', () => {
const stages = createStages();
const restore = Object.fromEntries(
Object.entries(stages).map(([name, stage]) => [name, stage.uniforms.intensity]),
Object.entries(stages).map(([name, stage]) => [
name,
stage.uniforms.intensity,
]),
);
assert.equal(applyCockpitVisionStageIntensities(stages, 'noir', restore), 'noir');
assert.equal(applyCockpitVisionStageIntensities(stages, 'noir'), 'noir');
assert.equal(stages.noir.uniforms.intensity, 1);
assert.equal(stages.retro.uniforms.intensity, 0);
applyCockpitVisionStageIntensities(stages, 'optical', restore);
assert.equal(stages.noir.uniforms.intensity, 0.72);
assert.equal(stages.retro.uniforms.intensity, 0.18);
applyCockpitVisionStageIntensities(stages, 'optical');
assert.equal(stages.noir.uniforms.intensity, 0);
assert.equal(stages.retro.uniforms.intensity, 0);
assert.deepEqual(restore, {
noir: 0.72,
retro: 0.18,
surveillance: 0,
thermal: 0,
});
});
+2
View File
@@ -1359,6 +1359,8 @@ export class StyleManager extends ShellFacade {
* @param {string} styleName - Target style ('normal'|'retro'|'surveillance'|'thermal'|'anime'|'noir'|'snow').
* @param {object} [options]
* @param {boolean} [options.applyPreset=true] - Whether to apply STYLE_PRESET_DEFAULTS for the new style.
* @param {boolean} [options.userInitiated=false] - Whether a direct user preset choice owns the Cyber exit style.
* @param {boolean} [options.preserveCyberRestore=false] - Whether an automatic style update keeps the Cyber entry snapshot.
* @returns {void}
*/
setStyle(
+5 -5
View File
@@ -52,11 +52,11 @@ export class CockpitViewController {
{
services,
onVisionChange = null,
getInitialVisionMode = null,
onCameraTakeover = null,
isEntryAllowed = null,
onEntered = null,
onExited = null,
getInheritedVisionLabel = null,
restoreTrackingFrame = null,
} = {},
) {
@@ -121,13 +121,13 @@ export class CockpitViewController {
this.visionNext = document.getElementById('cockpit-vision-next');
this.visionMode = 'optical';
this.onVisionChange = onVisionChange;
this.getInitialVisionMode =
typeof getInitialVisionMode === 'function'
? getInitialVisionMode
: () => 'optical';
this.onCameraTakeover = onCameraTakeover;
this.onEntered = onEntered;
this.onExited = onExited;
this.getInheritedVisionLabel =
typeof getInheritedVisionLabel === 'function'
? getInheritedVisionLabel
: () => 'NORMAL';
this.restoreTrackingFrame =
typeof restoreTrackingFrame === 'function'
? restoreTrackingFrame
+2 -4
View File
@@ -1,8 +1,8 @@
import { CockpitViewController, CockpitDisplayPortal } from './cockpit.js';
import { STYLE_STATUS_LABELS } from './visualPresets.js';
import { cockpitEntryAllowed } from '../contextModePolicy.js';
import { formatAwarenessLabel } from '../data/militaryAwarenessEngine.js';
import { enterCockpitWithTracking } from '../cockpitTracking.js';
import { cockpitVisionModeForStyle } from '../cockpitVisionPolicy.js';
/** Own Cockpit entry/rollback, readouts and the single Display portal. */
export class CockpitCoordinator {
@@ -72,11 +72,9 @@ export class CockpitCoordinator {
},
onVisionChange: (mode, active, options) =>
this._setCockpitVision(mode, active, options),
getInitialVisionMode: () => cockpitVisionModeForStyle(this.activeStyle),
onCameraTakeover: () =>
this._stampNavigation({ cancelPendingSelection: false }),
getInheritedVisionLabel: () =>
STYLE_STATUS_LABELS[this.activeStyle] ||
String(this.activeStyle || 'normal').toUpperCase(),
isEntryAllowed: () =>
cockpitEntryAllowed({
contextMode: this._contextMode,
+6 -5
View File
@@ -217,22 +217,23 @@ export function syncWeatherToggle(enabled) {
export function setVisionMode(mode, { revealParameters = false } = {}) {
const next = normalizeCockpitVisionMode(mode);
this.visionMode = next;
const inherited = String(
this.getInheritedVisionLabel?.() || 'NORMAL',
).toUpperCase();
const labels = {
optical: inherited,
optical: 'NORMAL',
crt: 'CRT',
nvg: 'NVG',
thermal: 'FLIR',
anime: 'ANIME',
noir: 'NOIR',
snow: 'SNOW',
};
const names = {
optical: inherited,
optical: 'Normal',
crt: 'CRT',
nvg: 'Night vision',
thermal: 'Thermal',
anime: 'Anime',
noir: 'Noir',
snow: 'Snow',
};
if (this.visionCurrent) {
this.visionCurrent.dataset.cockpitVision = next;
+1 -1
View File
@@ -194,7 +194,7 @@ export function enter() {
`${trackLabel} · COURSE ${trackHeading}°`,
);
this.updateHud(info, performance.now(), true);
this.setVisionMode(this.visionMode);
this.setVisionMode(this.getInitialVisionMode());
this.scheduleContextLayout();
this.mapViewButton?.focus({ preventScroll: true });
this.onEntered?.();
+95 -12
View File
@@ -140,23 +140,74 @@ test('non-Cyber style presets and explicit HUD choices remain authoritative', (t
}
});
test('leaving an explicitly selected Cyber layout restores the prior visual preset', (t) => {
test('leaving Cyber without a preset choice restores the prior visual preset', (t) => {
const { owner, hud } = styleOwner(t, 'tactical');
owner.setStyle('noir');
owner._setHudVariant('cyber', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'thermal');
assert.equal(owner._preCyberStyle, 'noir');
assert.equal(owner._cyberStyleUserSelected, false);
// A temporary choice made while Cyber is active does not replace the preset
// that was active before the user entered Cyber.
owner.setStyle('snow');
owner._setHudVariant('cyber', { applyVisualDefaults: true });
assert.equal(owner._preCyberStyle, 'noir');
owner._setHudVariant('operator', { applyVisualDefaults: true });
assert.equal(hud.getVariant(), 'operator');
assert.equal(owner.activeStyle, 'noir');
assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false);
});
test('a preset explicitly selected in Cyber survives exit and resets for re-entry', (t) => {
const { owner, hud } = styleOwner(t, 'tactical');
owner.setStyle('noir');
owner._setHudVariant('cyber', { applyVisualDefaults: true });
owner.setStyle('snow', { userInitiated: true });
assert.equal(owner.activeStyle, 'snow');
assert.equal(owner._preCyberStyle, 'noir');
assert.equal(owner._cyberStyleUserSelected, true);
// Reselecting the active mode must not erase the user's preset intent.
owner._setHudVariant('cyber', { applyVisualDefaults: true });
assert.equal(owner._preCyberStyle, 'noir');
owner._setHudVariant('operator', { applyVisualDefaults: true });
assert.equal(hud.getVariant(), 'operator');
assert.equal(owner.activeStyle, 'snow');
assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false);
// A later untouched Cyber round trip restores the new pre-Cyber preset.
owner._setHudVariant('cyber', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'thermal');
assert.equal(owner._preCyberStyle, 'snow');
owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'snow');
});
test('an explicit same-value FLIR choice is preserved on Cyber exit', (t) => {
const { owner } = styleOwner(t, 'tactical');
owner.setStyle('noir');
owner._setHudVariant('cyber', { applyVisualDefaults: true });
owner.setStyle('thermal', { userInitiated: true });
assert.equal(owner.activeStyle, 'thermal');
assert.equal(owner._cyberStyleUserSelected, true);
owner._setHudVariant('operator', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'thermal');
});
test('an authoritative same-Cyber reset clears local preset ownership', (t) => {
const { owner } = styleOwner(t, 'tactical');
owner.setStyle('noir');
owner._setHudVariant('cyber', { applyVisualDefaults: true });
owner.setStyle('snow', { userInitiated: true });
assert.equal(owner._cyberStyleUserSelected, true);
owner._setHudVariant('cyber');
assert.equal(owner.activeStyle, 'snow');
assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false);
owner._setHudVariant('operator', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'snow');
});
test('Display and voice HUD routes preserve explicit Cyber entry and exit authority', async (t) => {
@@ -174,17 +225,25 @@ test('Display and voice HUD routes preserve explicit Cyber entry and exit author
const display = makeOwner();
const hudLayout = new EventTarget();
hudLayout.value = 'cyber';
const snowButton = new EventTarget();
snowButton.dataset = { style: 'snow' };
const controls = bindDisplayControls({
elements: { hudLayout },
actions: { setHudLayout: (value) => display.owner.setHudLayout(value) },
elements: { hudLayout, styleButtons: [snowButton] },
actions: {
setHudLayout: (value) => display.owner.setHudLayout(value),
setStyle: (value) =>
display.owner.setStyle(value, { userInitiated: true }),
},
});
hudLayout.dispatchEvent(new Event('change'));
assert.equal(display.hud.getVariant(), 'cyber');
assert.equal(display.owner.activeStyle, 'thermal');
snowButton.dispatchEvent(new Event('click'));
assert.equal(display.owner.activeStyle, 'snow');
hudLayout.value = 'operator';
hudLayout.dispatchEvent(new Event('change'));
assert.equal(display.hud.getVariant(), 'operator');
assert.equal(display.owner.activeStyle, 'noir');
assert.equal(display.owner.activeStyle, 'snow');
controls.destroy();
const voice = makeOwner();
@@ -203,27 +262,34 @@ test('Display and voice HUD routes preserve explicit Cyber entry and exit author
assert.equal(entered.ok, true);
assert.equal(voice.hud.getVariant(), 'cyber');
assert.equal(voice.owner.activeStyle, 'thermal');
const selected = await runVoiceAction('set_visual_style', { style: 'snow' });
assert.equal(selected.ok, true);
assert.equal(voice.owner.activeStyle, 'snow');
const exited = await runVoiceAction('set_hud', { layout: 'operator' });
assert.equal(exited.ok, true);
assert.equal(voice.hud.getVariant(), 'operator');
assert.equal(voice.owner.activeStyle, 'noir');
assert.equal(voice.owner.activeStyle, 'snow');
});
test('scene and share HUD transitions do not restore a local pre-Cyber preset', async (t) => {
const { owner, hud } = styleOwner(t, 'tactical');
owner.setStyle('noir');
owner._setHudVariant('cyber', { applyVisualDefaults: true });
owner.setStyle('snow', { userInitiated: true });
assert.equal(owner._cyberStyleUserSelected, true);
await owner.applyVisualState({ style: 'retro', hud: { variant: 'cyber' } });
assert.equal(hud.getVariant(), 'cyber');
assert.equal(owner.activeStyle, 'retro');
assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false);
owner._setHudVariant('operator', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'retro');
owner.setStyle('surveillance');
owner._setHudVariant('cyber', { applyVisualDefaults: true });
assert.equal(owner._preCyberStyle, 'surveillance');
owner.setStyle('snow', { userInitiated: true });
await owner.restoreShareState({
style: 'thermal',
hudVariant: 'cyber',
@@ -231,6 +297,7 @@ test('scene and share HUD transitions do not restore a local pre-Cyber preset',
assert.equal(hud.getVariant(), 'cyber');
assert.equal(owner.activeStyle, 'thermal');
assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false);
owner._setHudVariant('tactical', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'thermal');
assert.equal(owner._preCyberStyle, null);
@@ -451,6 +518,14 @@ test('Cyber side panels share one width and one framed surface material', () =>
cyberStyles,
/#pp-toggles\s*> \.pp-header-row\s+\.pp-collapse-btn \{[\s\S]*?width: var\(--cyber-panel-control-size\);[\s\S]*?height: var\(--cyber-panel-control-size\);/,
);
assert.match(
cyberStyles,
/#right-context-rail\s+:is\(\s*\.cctv-panel-inner,\s*\.weather-panel-inner,\s*\.recent-imagery-panel-inner,\s*\.global-context-panel-inner\s*\) \{\s*padding-right: 8px;/,
);
assert.match(
cyberStyles,
/#right-context-rail[\s\S]*?:is\(\s*#cctv-panel,\s*#weather-panel,\s*#recent-imagery-panel,\s*#global-context-panel\s*\)\.collapsed[\s\S]*?> \.panel-header \{[\s\S]*?height: 48px;[\s\S]*?padding: 0 10px 0 14px;/,
);
assert.doesNotMatch(
cyberStyles,
/\.cockpit-utility-control\.is-expanded\s*\{[^}]*order:/,
@@ -518,7 +593,15 @@ test('Cyber visual defaults update the style, thermal palette and render state',
assert.equal(owner.stages.thermal.uniforms.palette, 0.42);
assert.deepEqual(calls, [
['style', 'thermal', { applyPreset: false, revealParameters: false }],
[
'style',
'thermal',
{
applyPreset: false,
revealParameters: false,
preserveCyberRestore: true,
},
],
['sliders', 'thermal', { reveal: false }],
['render', 'cyber-visual-defaults'],
]);
@@ -726,7 +809,7 @@ test('Cyber desktop cockpit collapsed utility frames match the 50px Data Layers
);
assert.match(
desktop,
/:root\[data-ui-theme='cyber'\]\s+body\.cockpit-mode\s+\.cockpit-utility-control:not\(\.is-expanded\)\s+\.cockpit-utility-launcher\s*\{\s*height: 50px;\s*min-height: 50px;\s*padding: 8px 18px 10px 24px;/,
/:root\[data-ui-theme='cyber'\]\s+body\.cockpit-mode\s+\.cockpit-utility-control:not\(\.is-expanded\)\s+\.cockpit-utility-launcher\s*\{\s*height: 50px;\s*min-height: 50px;\s*padding: 9px 18px 9px 24px;/,
);
assert.match(
desktop,
+2 -2
View File
@@ -64,7 +64,7 @@ export class DisplayBindings {
documentRef: document,
searchInput: this._locationSearch,
actions: {
setStyle: (style) => this.setStyle(style),
setStyle: (style) => this.setStyle(style, { userInitiated: true }),
dismissSearch: () => {
if (this._locationSearch.classList.contains('expanded')) {
this._locationSearch.classList.remove('expanded');
@@ -121,7 +121,7 @@ export class DisplayBindings {
modelModeButtons: this._models3dBtn ? this._models3dModeBtns : [],
},
actions: {
setStyle: (style) => this.setStyle(style),
setStyle: (style) => this.setStyle(style, { userInitiated: true }),
toggleBloom: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setBloomEnabled(!this.bloomEnabled);
+1 -1
View File
@@ -655,7 +655,7 @@ export class ShellFacade {
return this._visualSettings._syncIrBoost(...arguments);
}
/** Keep Cockpit's inherited label and restore target aligned with the active map preset. */
/** Keep Cockpit's exit restore target aligned with the active map preset. */
_syncCockpitInheritedStyle() {
return this._visualSettings._syncCockpitInheritedStyle(...arguments);
}
+60 -1
View File
@@ -229,6 +229,65 @@
border-bottom: 1px solid rgba(206, 222, 226, 0.08);
}
/* The right-rail surfaces use different content gutters, but their disclosure
controls should still read as one vertical column. Eight pixels here plus
the shared two-pixel header inset matches Display's eleven-pixel framed
edge inset once the surface border is included. */
:root[data-ui-theme='cyber']
#right-context-rail
:is(
.cctv-panel-inner,
.weather-panel-inner,
.recent-imagery-panel-inner,
.global-context-panel-inner
) {
padding-right: 8px;
}
/* Collapsed right-rail panels otherwise inherit different legacy gutters:
CCTV lands five pixels low while Context lands four pixels high. Give every
compact launcher the same centered 48px content row inside its 1px frame. */
:root[data-ui-theme='cyber']
#right-context-rail
:is(
#cctv-panel,
#weather-panel,
#recent-imagery-panel,
#global-context-panel
).collapsed
> :is(
.cctv-panel-inner,
.weather-panel-inner,
.recent-imagery-panel-inner,
.global-context-panel-inner
) {
padding: 0;
}
:root[data-ui-theme='cyber']
#right-context-rail
:is(
#cctv-panel,
#weather-panel,
#recent-imagery-panel,
#global-context-panel
).collapsed
> :is(
.cctv-panel-inner,
.weather-panel-inner,
.recent-imagery-panel-inner,
.global-context-panel-inner
)
> .panel-header {
box-sizing: border-box;
width: 100%;
height: 48px;
min-height: 48px;
margin: 0;
padding: 0 10px 0 14px;
border-bottom: 0;
}
:root[data-ui-theme='cyber']
:is(
.data-panel-inner,
@@ -1623,7 +1682,7 @@
.cockpit-utility-launcher {
height: 50px;
min-height: 50px;
padding: 8px 18px 10px 24px;
padding: 9px 18px 9px 24px;
}
:root[data-ui-theme='cyber']
+33 -15
View File
@@ -197,6 +197,7 @@ export class VisualSettings {
});
this.activeStyle = 'normal';
this._preCyberStyle = null;
this._cyberStyleUserSelected = false;
document.documentElement.dataset.gevStyle = this.activeStyle;
this._detectionUserOverridden = false;
this._cockpitVisionMode = 'optical';
@@ -342,23 +343,15 @@ export class VisualSettings {
);
}
if (next === 'optical') {
applyCockpitVisionStageIntensities(
this.stages,
next,
this._cockpitVisionRestore,
);
applyCockpitVisionStageIntensities(this.stages, next);
this._syncStagesEnabledFromIntensity();
this._cockpitVisionMode = next;
this._syncIrBoost();
this._updateSliderPanel(this.activeStyle, { reveal: false });
this._updateSliderPanel(null, { reveal: false });
this._revealCockpitStyleParameters({ openDisplay: revealParameters });
return;
}
const target = applyCockpitVisionStageIntensities(
this.stages,
next,
this._cockpitVisionRestore,
);
const target = applyCockpitVisionStageIntensities(this.stages, next);
this._syncStagesEnabledFromIntensity();
this._cockpitVisionMode = next;
this._syncIrBoost(); // Cockpit vision override ('nvg'/'thermal' boost; CRT/NOIR clear)
@@ -370,8 +363,11 @@ export class VisualSettings {
const cockpitMode = this.cockpitView?.active
? this._cockpitVisionMode
: null;
const effective =
cockpitMode && cockpitMode !== 'optical' ? cockpitMode : this.activeStyle;
const effective = cockpitMode
? cockpitMode === 'optical'
? 'normal'
: cockpitMode
: this.activeStyle;
const irBoost =
effective === 'surveillance' ||
effective === 'thermal' ||
@@ -385,7 +381,7 @@ export class VisualSettings {
new CustomEvent('gev:vision-change', {
detail: {
style: effective,
cockpit: Boolean(cockpitMode && cockpitMode !== 'optical'),
cockpit: Boolean(cockpitMode),
},
}),
);
@@ -622,8 +618,10 @@ export class VisualSettings {
// 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;
}
if (this._hudLayoutSelect && this._hudLayoutSelect.value !== nextVariant) {
this._hudLayoutSelect.value = nextVariant;
@@ -635,12 +633,17 @@ export class VisualSettings {
);
if (visualDefaults) this._applyCyberVisualDefaults(visualDefaults);
if (leftCyber) {
const restoreStyle = applyVisualDefaults ? this._preCyberStyle : null;
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,
});
}
}
@@ -653,6 +656,7 @@ export class VisualSettings {
this.setStyle(style, {
applyPreset: false,
revealParameters: false,
preserveCyberRestore: true,
});
const thermalUniforms = this.stages?.thermal?.uniforms;
if (!thermalUniforms || thermalUniforms.palette === undefined) return;
@@ -1530,10 +1534,24 @@ export class VisualSettings {
applyPreset = true,
revealParameters = applyPreset,
restore = false,
userInitiated = false,
preserveCyberRestore = false,
} = {},
) {
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();
+1 -1
View File
@@ -688,7 +688,7 @@ export function createGevActionRunner({
const style = normalizeStyle(args.style);
if (!style)
throw new Error(`Unknown visual style: ${args.style || 'missing'}`);
styleManager.setStyle(style);
styleManager.setStyle(style, { userInitiated: true });
return { ok: true, action: 'set_visual_style', style };
}