mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge upstream/main (v0.2.1) into pr/mapillary-layer
Brings in 123 commits from main, including the v0.2.0 and v0.2.1 releases (MCP server and tool catalog, server security hardening, build hygiene). Conflicts: - package.json: kept main's script order and added this PR's qa:panel-resize and qa:street-level; exports keep both sides (main's tools/view entries and this PR's street-level and Mapillary entries). - src/tooling/viteBuild.test.mjs: kept this PR's Mapillary-token define assertion and main's new wildcard-host allowedHosts assertions. - CHANGELOG.md: main now has release sections; this PR's entries go under [Unreleased]. Checks after the merge: doctor, format:check, check:boundaries, layer-token:check (28 published, 1 new: street-level 0), npm test (5726 pass), build, the Windows job's focused tests (70 pass), qa:panel-resize, and the keyless street-level gate. The new CSP allows Mapillary (https: connect/img, blob workers). The keyed gate stops at coverage locally because the dev .env Mapillary token is invalid (upstream 403), not because of the merge. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
5a7ce5785e
176 files changed
+19654
-1431
No files matched your search
@@ -1388,6 +1388,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?.();
|
||||
|
||||
+290
-6
@@ -9,6 +9,9 @@ import {
|
||||
normalizeHudLayout,
|
||||
} from '../hudLayouts.js';
|
||||
import { GEV_ACTION_SCHEMAS } from '../voice/actionSchemas.js';
|
||||
import { createGevActionRunner } from '../voice/gevActions.js';
|
||||
import { StyleManager } from './applicationShell.js';
|
||||
import { bindDisplayControls } from './displayControls.js';
|
||||
import { VisualSettings } from './visualSettings.js';
|
||||
import { STYLES } from './visualPresets.js';
|
||||
|
||||
@@ -137,6 +140,169 @@ test('non-Cyber style presets and explicit HUD choices remain authoritative', (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);
|
||||
|
||||
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) => {
|
||||
const makeOwner = () => {
|
||||
const { owner, hud } = styleOwner(t, 'tactical');
|
||||
owner.setStyle('noir');
|
||||
owner.setHudLayout = (...args) =>
|
||||
StyleManager.prototype.setHudLayout.call(owner, ...args);
|
||||
owner.getControlState = () => ({
|
||||
hud: { layout: hud.getVariant(), visible: true },
|
||||
});
|
||||
return { owner, hud };
|
||||
};
|
||||
|
||||
const display = makeOwner();
|
||||
const hudLayout = new EventTarget();
|
||||
hudLayout.value = 'cyber';
|
||||
const snowButton = new EventTarget();
|
||||
snowButton.dataset = { style: 'snow' };
|
||||
const controls = bindDisplayControls({
|
||||
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, 'snow');
|
||||
controls.destroy();
|
||||
|
||||
const voice = makeOwner();
|
||||
const eventSource = { addEventListener() {}, removeEventListener() {} };
|
||||
const viewer = {
|
||||
camera: { moveEnd: { addEventListener() {} } },
|
||||
clock: { onTick: { addEventListener: () => () => {} } },
|
||||
scene: { canvas: eventSource },
|
||||
};
|
||||
const runVoiceAction = createGevActionRunner({
|
||||
viewer,
|
||||
styleManager: voice.owner,
|
||||
dataManager: {},
|
||||
});
|
||||
const entered = await runVoiceAction('set_hud', { layout: 'cyber' });
|
||||
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, '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',
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
test('scene and share restoration override Cyber and preserve restored Cyber parameters', async (t) => {
|
||||
const { owner, hud } = styleOwner(t);
|
||||
await owner.applyVisualState({ style: 'retro', hud: { variant: 'minimal' } });
|
||||
@@ -179,6 +345,104 @@ test('Cyber setup uses a compact toolbar icon and highlights the north-up letter
|
||||
});
|
||||
const cyberStyles = read('./styles/cyber.css');
|
||||
|
||||
// Source guards supplement rendered popup/hit-target checks. Inspect one rule
|
||||
// at a time so a safe declaration on a child cannot excuse a clipped ancestor.
|
||||
function cssRuleBody(styles, selector) {
|
||||
const match = styles.match(new RegExp(`${selector.source}\\s*\\{([^}]+)\\}`));
|
||||
assert.ok(match, `Missing CSS rule: ${selector.source}`);
|
||||
return match[1];
|
||||
}
|
||||
|
||||
test('Cyber Voice clips its decoration without clipping descendant help and errors', () => {
|
||||
const owner = cssRuleBody(
|
||||
cyberStyles,
|
||||
/:root\[data-ui-theme='cyber'\] #command-dock > #gev-voice-control/,
|
||||
);
|
||||
assert.match(owner, /isolation:\s*isolate;/);
|
||||
assert.match(owner, /overflow:\s*visible;/);
|
||||
assert.match(owner, /clip-path:\s*none;/);
|
||||
|
||||
const decoration = cssRuleBody(
|
||||
cyberStyles,
|
||||
/:root\[data-ui-theme='cyber'\] #command-dock > #gev-voice-control::before/,
|
||||
);
|
||||
assert.match(decoration, /clip-path:\s*polygon\(/);
|
||||
assert.match(decoration, /pointer-events:\s*none;/);
|
||||
assert.match(decoration, /inset:\s*0;/);
|
||||
assert.match(decoration, /z-index:\s*-1;/);
|
||||
});
|
||||
|
||||
test('Cyber dock pins escape the tray mask and its decoration overrides the legacy arrow geometry', () => {
|
||||
const owner = cssRuleBody(
|
||||
cyberStyles,
|
||||
/:root\[data-ui-theme='cyber'\] #command-dock \.dock-popover-content/,
|
||||
);
|
||||
assert.match(owner, /clip-path:\s*none;/);
|
||||
assert.match(owner, /isolation:\s*isolate;/);
|
||||
|
||||
// Including the tray owners beats #control-panel's more specific old
|
||||
// ::after arrow positioning. Otherwise left:auto can collapse the frame.
|
||||
const decoration = cssRuleBody(
|
||||
cyberStyles,
|
||||
/:root\[data-ui-theme='cyber'\]\s+#command-dock\s+:is\(#location-bar, #control-panel\)\s+\.dock-popover-content::after/,
|
||||
);
|
||||
assert.match(decoration, /inset:\s*0;/);
|
||||
assert.match(decoration, /width:\s*auto;/);
|
||||
assert.match(decoration, /height:\s*auto;/);
|
||||
assert.match(decoration, /transform:\s*none;/);
|
||||
assert.match(decoration, /clip-path:\s*var\(--cyber-panel-frame\);/);
|
||||
assert.match(decoration, /pointer-events:\s*none;/);
|
||||
});
|
||||
|
||||
test('Cyber lower-left telemetry tracks the credit anchor with clearance for its logo row', () => {
|
||||
const telemetry = cssRuleBody(
|
||||
cyberStyles,
|
||||
/#intel-hud\[data-variant='cyber'\] \.hud-bottom-left/,
|
||||
);
|
||||
const offset = telemetry.match(/bottom:\s*calc\(2vh \+ ([\d.]+)rem\);/);
|
||||
assert.ok(offset, 'telemetry must retain the attribution viewport anchor');
|
||||
const credits = read('./styles/command-dock-trays.css');
|
||||
const creditOffsets = [
|
||||
...credits.matchAll(
|
||||
/#cesium-credits\s*\{[^}]*bottom:\s*calc\(2vh \+ ([\d.]+)rem\);/g,
|
||||
),
|
||||
];
|
||||
assert.ok(creditOffsets.length > 0);
|
||||
for (const credit of creditOffsets) {
|
||||
assert.ok(
|
||||
Number(offset[1]) - Number(credit[1]) >= 2.75,
|
||||
'reserve the 28px logo row, baseline descent and 12px clearance at the 16px root size',
|
||||
);
|
||||
}
|
||||
assert.doesNotMatch(cyberStyles, /\.hud-bottom-left\s*\{\s*bottom:\s*60px;/);
|
||||
});
|
||||
|
||||
test('shared panel hooks affect only opted-in rail surfaces without overriding disclosure and concealment', () => {
|
||||
const styles = read('./styles/panel-surfaces.css');
|
||||
const rail = cssRuleBody(
|
||||
styles,
|
||||
/:is\(#left-panel-stack, #right-context-rail\)\s+> \[data-panel-id\]:has\(> \[data-panel-surface\]\)/,
|
||||
);
|
||||
assert.match(rail, /pointer-events:\s*auto;/);
|
||||
assert.doesNotMatch(rail, /(?:display|visibility):/);
|
||||
assert.doesNotMatch(
|
||||
styles,
|
||||
/:is\(#left-panel-stack, #right-context-rail\)\s*> \[data-panel-id\]\s*\{/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/\[data-panel-surface\]:not\(\[hidden\]\):not\(dialog:not\(\[open\]\)\)\s*\{\s*display: flex;/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/dialog\[data-panel-surface\]:not\(\[open\]\)\s*\{\s*display: none !important;/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/body:is\(\.ui-clean-view, \.recording-mode\)[^{]+\{[^}]*visibility: hidden !important;[^}]*pointer-events: none !important;/,
|
||||
);
|
||||
});
|
||||
|
||||
test('Cyber clipped expand controls retain an inset keyboard-focus outline in map and cockpit', () => {
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
@@ -254,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:/,
|
||||
@@ -321,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'],
|
||||
]);
|
||||
@@ -330,6 +610,7 @@ test('Cyber visual defaults update the style, thermal palette and render state',
|
||||
test('the Display selector, voice schema and final stylesheet expose Cyber', () => {
|
||||
const display = read('./templates/display-controls.html');
|
||||
const stylesheet = read('../../style.css');
|
||||
const rightRail = read('./rightPanelRail.js');
|
||||
const hud = read('../hud.js');
|
||||
const detection = read('../data/detection.js');
|
||||
const civilRendering = read('../layers/flights/rendering.js');
|
||||
@@ -415,21 +696,24 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
||||
assert.match(cyberStyles, /--cyber-hud-card-top: max\(100px, 10\.25vh\);/);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/--cyber-left-rail-top: max\([\s\S]*?252px,[\s\S]*?calc\(var\(--cyber-hud-card-top\) \+ 146px\)/,
|
||||
/--cyber-left-rail-top: max\([\s\S]*?220px,[\s\S]*?calc\(var\(--cyber-hud-card-top\) \+ 120px\)/,
|
||||
);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/\.hud-top-left \{[\s\S]*?top: var\(--cyber-hud-card-top\);/,
|
||||
);
|
||||
assert.match(cyberStyles, /\.hud-bottom-left \{\s*bottom: 60px;/);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/#left-panel-stack \{[\s\S]*?top: var\(--cyber-left-rail-top\) !important;/,
|
||||
/#left-panel-stack \{[\s\S]*?top: var\(--cyber-left-rail-top\) !important;[\s\S]*?bottom: calc\(2vh \+ 13rem\);/,
|
||||
);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/\.hud-left-edge \{\s*top: calc\(var\(--cyber-left-rail-top\) - 18px\);\s*left: 14px;/,
|
||||
);
|
||||
assert.match(
|
||||
rightRail,
|
||||
/const leftStackTop = leftStack\?\.getBoundingClientRect\(\)\.top;[\s\S]*?const alignedTop = Number\.isFinite\(leftStackTop\)[\s\S]*?baseTop: alignedTop,/,
|
||||
);
|
||||
assert.match(
|
||||
cyberStyles,
|
||||
/--right-display-collapsed-width: var\(--cyber-panel-collapsed-width\);/,
|
||||
@@ -453,7 +737,7 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
||||
assert.match(cyberStyles, /#left-panel-stack:has/);
|
||||
assert.match(cyberStyles, /left: 54px;/);
|
||||
assert.match(cyberStyles, /right: 62px;/);
|
||||
assert.match(cyberStyles, /bottom: calc\(2vh \+ 11rem\);/);
|
||||
assert.match(cyberStyles, /bottom: calc\(2vh \+ 13rem\);/);
|
||||
assert.doesNotMatch(cyberStyles, /NAV 01 \/\/ COMMAND BUS \/\/ VIS 03/);
|
||||
assert.match(cyberStyles, /data-layer-id='satellites'/);
|
||||
assert.match(cyberStyles, /#search-toggle::before/);
|
||||
@@ -525,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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LayerStateCoordinator } from '../data/layerState.js';
|
||||
import { LayerStateCoordinator } from '../data/layerStateCoordinator.js';
|
||||
import { stampInitialShareGesture } from '../navigationPolicy.js';
|
||||
import { canPresentDeferredStatusNotice } from '../loadingFeedback.js';
|
||||
import { UiLifetime } from './uiLifetime.js';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+123
-11
@@ -185,6 +185,11 @@
|
||||
0 8px 20px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
/* Compact Cyber rails must wrap multi-word actions within their own button. */
|
||||
:root[data-ui-theme='cyber'] #scene-panel .scene-controls .scene-btn {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Carry the raised voice-module silhouette across the primary panel system.
|
||||
The six-pixel calibration bay is decorative and stays outside panel content. */
|
||||
:root[data-ui-theme='cyber']
|
||||
@@ -224,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,
|
||||
@@ -924,7 +988,24 @@
|
||||
--cyber-voice-wave: 105, 203, 209;
|
||||
--cyber-voice-peak: 226, 240, 240;
|
||||
--cyber-voice-alert: 196, 84, 84;
|
||||
border-color: rgba(196, 84, 84, 0.58);
|
||||
/* Popups extend above this root. Only its decorative layer may be clipped. */
|
||||
isolation: isolate;
|
||||
overflow: visible;
|
||||
clip-path: none;
|
||||
border-color: transparent;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] #command-dock > #gev-voice-control::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
border: 1px solid rgba(196, 84, 84, 0.58);
|
||||
border-radius: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(196, 84, 84, 0.14), transparent 34%),
|
||||
@@ -1192,8 +1273,30 @@
|
||||
|
||||
:root[data-ui-theme='cyber'] #command-dock .dock-popover-content {
|
||||
padding-top: 1.65rem;
|
||||
border: 1px solid rgba(196, 84, 84, 0.62);
|
||||
/* The pin straddles the tray edge; keep it outside the decorative mask. */
|
||||
isolation: isolate;
|
||||
clip-path: none;
|
||||
border-color: transparent;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber']
|
||||
#command-dock
|
||||
:is(#location-bar, #control-panel)
|
||||
.dock-popover-content::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
width: auto;
|
||||
height: auto;
|
||||
transform: none;
|
||||
pointer-events: none;
|
||||
border: 1px solid rgba(196, 84, 84, 0.62);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(62, 86, 94, 0.22), transparent 46%),
|
||||
linear-gradient(rgba(188, 211, 218, 0.045) 1px, transparent 1px),
|
||||
@@ -1456,6 +1559,12 @@
|
||||
bottom: calc(2vh + 6.5rem);
|
||||
}
|
||||
|
||||
/* Attribution's logo row extends above its text baseline. Reserve the whole
|
||||
row plus a gap above the dock-relative 2vh + 5rem credit position. */
|
||||
#intel-hud[data-variant='cyber'] .hud-bottom-left {
|
||||
bottom: calc(2vh + 7.75rem);
|
||||
}
|
||||
|
||||
#intel-hud[data-variant='cyber'] .hud-corner {
|
||||
position: fixed;
|
||||
isolation: isolate;
|
||||
@@ -1493,12 +1602,21 @@
|
||||
|
||||
/* Cyber also owns a distinct desktop composition. Existing components keep
|
||||
their controls and semantics, but move onto asymmetric mission-deck rails. */
|
||||
@media (min-width: 961px) and (max-height: 649px) {
|
||||
/* The compact-height layout drops the desktop rail composition one pixel
|
||||
below its 650px threshold. Keep the left rail inside the same telemetry
|
||||
and attribution-safe floor while its panel bodies take over scrolling. */
|
||||
:root[data-ui-theme='cyber'] #left-panel-stack {
|
||||
bottom: calc(2vh + 13rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 961px) and (min-height: 650px) {
|
||||
:root[data-ui-theme='cyber'] {
|
||||
/* Compose the upper mission card and both panel rails as one compact lane.
|
||||
The shared rail anchor keeps expanded left/right panels aligned. */
|
||||
--cyber-hud-card-top: max(100px, 10.25vh);
|
||||
--cyber-left-rail-top: max(252px, calc(var(--cyber-hud-card-top) + 146px));
|
||||
--cyber-left-rail-top: max(220px, calc(var(--cyber-hud-card-top) + 120px));
|
||||
}
|
||||
|
||||
/* Keep the rotated COLL / ONA telemetry in the upper clearance lane. Its
|
||||
@@ -1510,7 +1628,7 @@
|
||||
|
||||
:root[data-ui-theme='cyber'] #left-panel-stack {
|
||||
top: var(--cyber-left-rail-top) !important;
|
||||
bottom: calc(2vh + 11rem);
|
||||
bottom: calc(2vh + 13rem);
|
||||
left: 54px;
|
||||
width: 300px;
|
||||
padding-left: 10px;
|
||||
@@ -1564,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']
|
||||
@@ -1709,12 +1827,6 @@
|
||||
right: 28px;
|
||||
bottom: 28px;
|
||||
}
|
||||
|
||||
/* Match the lower telemetry cards by their top edge while leaving the
|
||||
shorter coordinate card clear of Cesium attribution. */
|
||||
#intel-hud[data-variant='cyber'] .hud-bottom-left {
|
||||
bottom: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Embed mode (?embed=1): the globe alone, on top of clean view. Provider
|
||||
attribution stays visible. */
|
||||
body.ui-embed #clean-view-exit,
|
||||
body.ui-embed #intel-hud,
|
||||
body.ui-embed #key-setup-chip,
|
||||
body.ui-embed #key-setup,
|
||||
body.ui-embed #first-run-launcher,
|
||||
body.ui-embed #command-dock,
|
||||
body.ui-embed #left-panel-stack,
|
||||
body.ui-embed #right-context-rail,
|
||||
body.ui-embed #world-overlay-actions,
|
||||
body.ui-embed #view-switcher {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/* Shared structural hooks for panels admitted by the existing rail owners.
|
||||
Specific legacy component rules retain priority; no panel IDs are registered
|
||||
here. The rail itself remains pointer-transparent over the globe. */
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id]:has(> [data-panel-surface]) {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
flex: 0 0 auto;
|
||||
min-height: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#left-panel-stack
|
||||
> [data-panel-id]:has(> [data-panel-surface]):not(.collapsed) {
|
||||
flex: 0 1 var(--left-panel-allocated-height, auto);
|
||||
height: var(--left-panel-allocated-height, auto);
|
||||
max-height: var(--left-panel-allocated-height, 100%);
|
||||
}
|
||||
|
||||
#left-panel-stack.layout-tail
|
||||
> [data-panel-id]:has(> [data-panel-surface]):not(.collapsed) {
|
||||
flex: 0 0 var(--left-panel-allocated-height);
|
||||
}
|
||||
|
||||
#right-context-rail.layout-focus
|
||||
> [data-panel-id]:has(> [data-panel-surface]):not(.collapsed) {
|
||||
flex: 0 1 var(--right-panel-allocated-height);
|
||||
height: var(--right-panel-allocated-height);
|
||||
max-height: var(--right-panel-allocated-height);
|
||||
}
|
||||
|
||||
/* Opt-in skin: the positioned surface can contain an outward popup. Only the
|
||||
non-interactive decoration is clipped; only data-panel-body scrolls. */
|
||||
:root {
|
||||
--panel-surface-font: var(--font-sans);
|
||||
--panel-surface-detail: none;
|
||||
--panel-surface-frame: none;
|
||||
--panel-surface-shadow: 0 8px 24px rgba(0, 0, 0, 0.38);
|
||||
--panel-surface-title: var(--text-secondary);
|
||||
--panel-surface-error: #ff9b8e;
|
||||
--panel-surface-width: 300px;
|
||||
--panel-surface-collapsed-width: var(--left-collapsed-width);
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] {
|
||||
--panel-surface-font: var(--font-mono);
|
||||
--panel-surface-detail: var(--cyber-panel-surface-detail);
|
||||
--panel-surface-frame: var(--cyber-panel-frame);
|
||||
--panel-surface-shadow: var(--cyber-panel-shadow);
|
||||
--panel-surface-title: var(--cyber-red-bright);
|
||||
--panel-surface-error: var(--cyber-red-bright);
|
||||
--panel-surface-width: var(--cyber-panel-width);
|
||||
--panel-surface-collapsed-width: var(--cyber-panel-collapsed-width);
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber']
|
||||
#right-context-rail
|
||||
> [data-panel-id]:has(> [data-panel-surface]) {
|
||||
--panel-surface-width: var(--cyber-right-panel-width);
|
||||
}
|
||||
|
||||
:is([data-panel-surface], [data-panel-popup]) {
|
||||
isolation: isolate;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--panel-radius);
|
||||
background: none;
|
||||
color: var(--text-primary);
|
||||
font: 12px/1.5 var(--panel-surface-font);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-panel-surface]:not(dialog) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:is([data-panel-surface], [data-panel-popup])::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
z-index: -1;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: inherit;
|
||||
background-color: var(--glass-bg);
|
||||
background-image: var(--panel-surface-detail);
|
||||
box-shadow: var(--panel-surface-shadow);
|
||||
clip-path: var(--panel-surface-frame);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* A closed native dialog must retain the browser's display:none. */
|
||||
[data-panel-surface]:not([hidden]):not(dialog:not([open])) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id]:has(> [data-panel-surface]) {
|
||||
width: min(100%, var(--panel-expanded-width, var(--panel-surface-width)));
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id]
|
||||
> [data-panel-surface] {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id].collapsed:has(> [data-panel-surface]) {
|
||||
width: min(
|
||||
100%,
|
||||
var(--panel-collapsed-width, var(--panel-surface-collapsed-width))
|
||||
);
|
||||
}
|
||||
|
||||
#right-context-rail[data-rail-measuring]
|
||||
> [data-panel-id]
|
||||
> [data-panel-surface] {
|
||||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
[data-panel-header] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
gap: 8px;
|
||||
min-height: 30px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.panel-surface-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: var(--panel-surface-title);
|
||||
font: 600 11px/1.4 var(--font-mono);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
[data-panel-body] {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--glass-border-hover) transparent;
|
||||
/* Keep an inset focus ring inside the intentional scrolling boundary. */
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.panel-surface-control {
|
||||
min-width: 0;
|
||||
min-height: 30px;
|
||||
max-width: 100%;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--btn-radius);
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
button.panel-surface-control {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panel-surface-control:hover:not(:disabled):not([aria-disabled='true']) {
|
||||
border-color: var(--glass-border-hover);
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
|
||||
.panel-surface-control:focus-visible {
|
||||
outline: 2px solid var(--text-primary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
.panel-surface-control:is(:disabled, [aria-disabled='true']) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.panel-surface-error,
|
||||
.panel-surface-control[aria-invalid='true'] {
|
||||
color: var(--panel-surface-error);
|
||||
border-color: var(--panel-surface-error);
|
||||
}
|
||||
|
||||
[data-panel-popup] {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
/* The owner supplies placement, width, collision handling and disclosure. */
|
||||
}
|
||||
|
||||
[data-panel-surface].collapsed > :is([data-panel-body], [data-panel-popup]),
|
||||
[data-panel-id].collapsed
|
||||
> [data-panel-surface]
|
||||
> :is([data-panel-body], [data-panel-popup]),
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id]:has(> [data-panel-surface])[hidden],
|
||||
:is(
|
||||
[data-panel-surface],
|
||||
[data-panel-header],
|
||||
[data-panel-body],
|
||||
[data-panel-popup]
|
||||
)[hidden],
|
||||
dialog[data-panel-surface]:not([open]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Conforming additions follow the same concealment policy as existing chrome.
|
||||
Cockpit continues to own rail/dock placement and exclusions in cockpit.css. */
|
||||
body:is(.ui-clean-view, .recording-mode)
|
||||
:is([data-panel-surface], [data-panel-popup]),
|
||||
body:is(.ui-clean-view, .recording-mode)
|
||||
:is(#left-panel-stack, #right-context-rail)
|
||||
> [data-panel-id]:has(> [data-panel-surface]) {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
+53
-14
@@ -196,6 +196,8 @@ export class VisualSettings {
|
||||
releaseRender: services.releaseContinuousRender,
|
||||
});
|
||||
this.activeStyle = 'normal';
|
||||
this._preCyberStyle = null;
|
||||
this._cyberStyleUserSelected = false;
|
||||
document.documentElement.dataset.gevStyle = this.activeStyle;
|
||||
this._detectionUserOverridden = false;
|
||||
this._cockpitVisionMode = 'optical';
|
||||
@@ -341,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)
|
||||
@@ -369,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' ||
|
||||
@@ -384,7 +381,7 @@ export class VisualSettings {
|
||||
new CustomEvent('gev:vision-change', {
|
||||
detail: {
|
||||
style: effective,
|
||||
cockpit: Boolean(cockpitMode && cockpitMode !== 'optical'),
|
||||
cockpit: Boolean(cockpitMode),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -614,6 +611,18 @@ export class VisualSettings {
|
||||
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;
|
||||
}
|
||||
if (this._hudLayoutSelect && this._hudLayoutSelect.value !== nextVariant) {
|
||||
this._hudLayoutSelect.value = nextVariant;
|
||||
}
|
||||
@@ -623,6 +632,21 @@ export class VisualSettings {
|
||||
{ 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 });
|
||||
@@ -632,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;
|
||||
@@ -1509,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();
|
||||
|
||||
Reference in new issue
Block a user