mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix(ui): preserve Cyber and Cockpit visual state
This commit is contained in:
1 parent
e03bf008f3
commit
1dbc8a4452
16 files changed
+361
-97
No files matched your search
+9
-2
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
- Cockpit now enters on the existing matching map style while keeping one fixed,
|
||||
duplicate-free carousel over `Normal / CRT / NVG / FLIR / Anime / Noir / Snow`.
|
||||
Normal remains a real unfiltered option; Cockpit-only choices still restore
|
||||
the captured map style through both Exit Cockpit and Reset.
|
||||
- Cyber's compact right-rail and Cockpit utility buttons now center their glyphs
|
||||
vertically and share the same inset and edge alignment.
|
||||
- Public Overpass instances are no longer used by default. Street Traffic
|
||||
roads come from TomTom flow tiles, OpenFreeMap vector tiles, or both, chosen
|
||||
on the layer row (TomTom / OSM / Hybrid) or with `?trafficRoads=`. With a
|
||||
@@ -157,8 +163,9 @@
|
||||
waves stay on-screen.
|
||||
- Keep Cyber Voice help/error popups and Location/Visual Presets pins clear of
|
||||
their decorative frames. Leave space above attribution for its full logo row.
|
||||
Add shared panel surface hooks for theme styling, rail input and bounded body
|
||||
scrolling without changing panel disclosure or visibility policies.
|
||||
- Add an intentionally future-facing, opt-in panel surface contract for new
|
||||
panels to inherit compatible Normal, Cyber and Cockpit styling. Its first
|
||||
production adopter will land separately after this change.
|
||||
- Restore the user's previous visual preset when they explicitly switch from
|
||||
Cyber to another HUD layout, without overriding scene or shared-link state.
|
||||
- Move the aligned Cyber side-panel rails upward on desktop so the left stack
|
||||
|
||||
+14
-11
@@ -23,9 +23,10 @@ Cyber frames without being clipped: the shapes decorate non-interactive
|
||||
pseudo-elements. The lower-left telemetry card leaves room for attribution's
|
||||
full logo row. New panels can use the [shared surface contract](panel-surfaces.md)
|
||||
for theme tokens, rail input and a fixed header with a bounded scroll body.
|
||||
Their existing owners still control disclosure, persistence and placement;
|
||||
small-screen rail popup clipping and other integration limits are documented
|
||||
in that contract.
|
||||
The contract is intentionally future-facing and opt-in; its first production
|
||||
adopter will land after this change. Existing owners still control disclosure,
|
||||
persistence and placement, while small-screen rail popup clipping and other
|
||||
integration limits remain documented in the contract.
|
||||
|
||||
On desktop, Cyber raises the left rail so it clears the lower coordinate card.
|
||||
`layoutRightPanelRail` reads that rendered top as its measured `baseTop`, which
|
||||
@@ -1958,11 +1959,12 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
|
||||
> registered for lifecycle and restoration but is not duplicated in Data Layers.
|
||||
> Inside Cockpit, the focused summary card is titled Contact in both visible
|
||||
> copy and its accessible control labels.
|
||||
> The top-center Cockpit vision cycle shows the inherited map preset name
|
||||
> (for example, `NOIR`) followed by CRT, NVG, FLIR, and NOIR. That inherited entry
|
||||
> leaves the selected map preset unchanged inside Cockpit. NONE is not offered
|
||||
> in the cycle; CRT, NVG, FLIR, and NOIR temporarily override that preset, while returning to it or exiting
|
||||
> Cockpit restores the captured map style and its exact shader intensities.
|
||||
> The top-center Cockpit vision cycle is the fixed, duplicate-free sequence
|
||||
> Normal, CRT, NVG, FLIR, Anime, Noir, and Snow. Entry selects the map preset
|
||||
> already in use, so a FLIR map opens on the existing FLIR item while
|
||||
> Normal remains available exactly once. NONE is not offered. Cockpit choices
|
||||
> temporarily override the map preset, while Exit Cockpit and Reset both restore
|
||||
> the captured map style and its exact shader intensities.
|
||||
> Selecting a Cockpit vision treatment with configurable parameters opens
|
||||
> Cockpit Display and reveals those parameters through the existing right-side
|
||||
> accordion; an inherited parameterless Normal preset does not force it open.
|
||||
@@ -3802,8 +3804,9 @@ and unreachable upstream (502/504) separately from road geometry.
|
||||
first, activates the Contacts sub-view, and returns the settled 250 km window.
|
||||
Its `aircraft` count is the exact civilian-plus-military total when both feeds
|
||||
can answer, or `unknown` when either component is unavailable.
|
||||
- Cockpit's top vision switch cycles five rendered looks: the inherited map
|
||||
style, CRT, NVG, FLIR, and Noir. There is no empty `NONE` entry.
|
||||
- Cockpit's top vision switch cycles all seven map looks: Normal, CRT, NVG,
|
||||
FLIR, Anime, Noir, and Snow. Entry selects the matching map style without adding
|
||||
a duplicate carousel item. There is no empty `NONE` entry.
|
||||
|
||||
### Live AIS Vessels (June 2026)
|
||||
|
||||
@@ -4174,7 +4177,7 @@ are omitted rather than framing the wrong part of the globe.
|
||||
- **Cockpit left-panel clearance:** the Cockpit Contact card and peripheral HUD participate in the adaptive left accordion's live obstacle measurements, including live viewport-height changes. Expanding Layers or Scenes keeps the active panel in the available upper-left corridor with internal scrolling; it does not cover the Contact card, lower Cockpit controls, or Cesium credit line. Outside Cockpit the hidden card does not alter the normal corridor.
|
||||
- **Cockpit Context scope:** the 250 km radius applies to the air/sea proximity cohorts. Installation counts come only from the currently loaded viewport and are labeled `CURRENT VIEWPORT ONLY` in the cockpit as well as the normal Context panel; neither surface presents them as a complete 250 km installation survey.
|
||||
- **Cockpit camera anchor:** first-person mode does not write feed-boundary corrections directly into the camera. A cockpit-only inertial anchor advances from the selected aircraft's displayed course and speed, then converges toward the authoritative delayed track with correction capped below forward motion. The displayed kinematics are derived from the same consecutive fix segment as the rendered position, with raw feed speed/course used only as fallback; a transient zero/missing feed speed therefore cannot freeze a visibly moving aircraft after layer enable or a map/cockpit handoff. Rendered altitude continues to come from that interpolated track position. Late ADS-B fixes and short render stalls can remove drift without accelerating or reversing the view. Camera placement runs before scene update/culling at a bounded 20 Hz so a moving cockpit does not force Photoreal 3D Tiles to retraverse on every display frame; textual instruments update at 10 Hz and context/layout work at 4 Hz. Every far Cockpit contact pip shares one stable Cesium texture-atlas entry and skips unused screen-projected course calculations, while only in-range 2D aircraft silhouettes pay the screen-projected rotation cost; ambient glTF collections are hidden/retained rather than synchronously destroyed at cockpit entry, and context rails lay out only on explicit content/state changes and viewport resize. The deliberate 15/30-second layer interpolation delays and per-Cesium-frame position caches remain unchanged.
|
||||
- **Cockpit route, vision, and view controls:** visible on-screen `COCKPIT`, `RESET`, and `EXIT COCKPIT` controls replace reliance on the `C` shortcut. RESET uses the same canonical globe route as the map and voice actions, exits Cockpit, and releases its camera ownership rather than exposing the hidden map-style top action. When the tracked commercial flight has a plausible ADSBDB route, the top of the right briefing rail shows a compact `FROM → TO` airport strip and the visor shows a centered estimated-destination chevron with its relative bearing; absent or implausible route data hides the strip and cue rather than guessing. The cockpit-local vision control is an interactive `PREV / CURRENT / NEXT` carousel over the inherited map preset, `CRT`, `NVG`, `FLIR`, and `NOIR`; its previous/next actions wrap, and activating the current value advances to the next style. The inherited entry is named directly, such as `NOIR`, and retains that map shader. There is no empty `NONE` entry. CRT, NVG, FLIR, and NOIR temporarily activate the existing Cesium post-process stages, while returning to the inherited entry or exiting Cockpit restores the pre-entry visual style. The regional-news page uses a free Google News RSS locality query first, with the existing GDELT query retained only as a fail-soft fallback; linked headlines remain reporting, not verified incidents or risk intelligence.
|
||||
- **Cockpit route, vision, and view controls:** visible on-screen `COCKPIT`, `RESET`, and `EXIT COCKPIT` controls replace reliance on the `C` shortcut. RESET uses the same canonical globe route as the map and voice actions, exits Cockpit, and releases its camera ownership rather than exposing the hidden map-style top action. When the tracked commercial flight has a plausible ADSBDB route, the top of the right briefing rail shows a compact `FROM → TO` airport strip and the visor shows a centered estimated-destination chevron with its relative bearing; absent or implausible route data hides the strip and cue rather than guessing. The cockpit-local vision control is an interactive `PREV / CURRENT / NEXT` carousel over `NORMAL`, `CRT`, `NVG`, `FLIR`, `ANIME`, `NOIR`, and `SNOW`; its previous/next actions wrap, and activating the current value advances to the next style. Cockpit enters on the active map preset, so a Cyber FLIR map opens Cockpit on FLIR while Normal remains a distinct option instead of producing a second FLIR entry. Cockpit choices temporarily activate the existing Cesium post-process stages, while exiting Cockpit through either EXIT or RESET restores the pre-entry map style. The regional-news page uses a free Google News RSS locality query first, with the existing GDELT query retained only as a fail-soft fallback; linked headlines remain reporting, not verified incidents or risk intelligence.
|
||||
- **Cockpit weather status:** the earlier multi-canvas atmospheric compositor remains fail-closed and is not attached to the live viewer. Cockpit clouds are a separate transparent WebGL pass with a capped 520×320 framebuffer, 24 ray steps, three FBM octaves, and a 12 FPS ceiling. It defaults off and starts only when local storage explicitly contains the persisted `WX ON` opt-in (`'1'`). When opted in, observations refresh after five minutes or 25 km of aircraft movement, fail transparent when unavailable or clear, and stop on exit or disable. `WX OFF` governs atmospheric rendering only: the briefing still fetches source-backed Natural Earth region, headline, and Open-Meteo local-information data, aborting and replacing any in-flight request when the selected aircraft changes. No weather effect runs in map mode and no synthetic fallback is shown.
|
||||
- **Cockpit trail visibility:** entering cockpit hides the selected aircraft's trail body and head so they cannot cross the first-person view; exit restores them. This cockpit-only presentation change does not alter the normal map-mode invariant that aircraft trails render through terrain using their depth-fail material.
|
||||
- **Aircraft course slew:** civilian and military 3D models retain the 60°/s course limiter, but each rendered frame can consume at most 250 ms of accumulated slew time. A long tile/render stall therefore catches up over multiple visible frames instead of turning one delayed frame into a heading snap.
|
||||
|
||||
@@ -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\(\);/);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user