mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
feat(ui): render the active style in the dock and the ACTIVE STYLE corner from the store
VisualSettings no longer toggles the style buttons' active class or writes the dock's style mini status and the ACTIVE STYLE corner. It publishes the active style and its label to the gev-display store; CommandDockView and SceneChromeView render them. The buttons keep their owner's listeners, and the initial label renders through t().
This commit is contained in:
1 parent
66a60ff67e
commit
decac29fbd
11 files changed
+146
-39
No files matched your search
@@ -115,7 +115,10 @@ They contain no application state.
|
||||
publishes through `subscribePanel()`, the first layer to hand its rail
|
||||
content to a view, and `MissionRoster` and `MissionDetail` the Space
|
||||
Missions roster and selected mission that the launches layer publishes
|
||||
the same way), the command dock,
|
||||
the same way), the command dock (in progress: the style buttons, the
|
||||
dock's style mini status and the ACTIVE STYLE corner render the style
|
||||
VisualSettings publishes to the `gev-display` store; the buttons keep
|
||||
their owner's listeners),
|
||||
the phone shell, then Cockpit. Layers that write into the right rail
|
||||
directly gain a slot instead.
|
||||
|
||||
|
||||
@@ -20,16 +20,17 @@ function locationSearchHandler() {
|
||||
|
||||
test('the ACTIVE STYLE indicator is written from the style name and nothing else', () => {
|
||||
// A free-text location search used to write the searched CITY into the
|
||||
// top-right style slot, so the corner read "ACTIVE STYLE / TOKYO".
|
||||
const writes = [...ui.matchAll(/this\._styleIndicator\.textContent\s*=/g)];
|
||||
// top-right style slot, so the corner read "ACTIVE STYLE / TOKYO". The
|
||||
// corner renders the style VisualSettings publishes, from one writer.
|
||||
const writes = [...ui.matchAll(/\.publishStyle\(/g)];
|
||||
assert.equal(writes.length, 1, 'the style indicator must have exactly one writer');
|
||||
assert.match(
|
||||
ui.slice(writes[0].index, writes[0].index + 160),
|
||||
/this\._styleIndicator\.textContent = STYLE_STATUS_LABELS\[styleName\]\s*\?\s*t\(STYLE_STATUS_LABELS\[styleName\]\)\s*:\s*styleName\.toUpperCase\(\);/,
|
||||
ui.slice(writes[0].index, writes[0].index + 200),
|
||||
/\.publishStyle\(\{\s*name,\s*label: STYLE_STATUS_LABELS\[name\]\s*\?\s*t\(STYLE_STATUS_LABELS\[name\]\)\s*:\s*name\.toUpperCase\(\),/,
|
||||
);
|
||||
|
||||
const handler = locationSearchHandler();
|
||||
assert.doesNotMatch(handler, /_styleIndicator/, 'location search must not touch the style indicator');
|
||||
assert.doesNotMatch(handler, /publishStyle|_publishActiveStyle/, 'location search must not touch the style indicator');
|
||||
assert.doesNotMatch(handler, /active-style-name/, 'location search must not touch the style indicator');
|
||||
});
|
||||
|
||||
|
||||
@@ -213,8 +213,6 @@ export class StyleManager extends ShellFacade {
|
||||
_ppToggles: this._ppToggles,
|
||||
_sliderContainer: this._sliderContainer,
|
||||
_sliderPanel: this._sliderPanel,
|
||||
_styleIndicator: this._styleIndicator,
|
||||
_styleMiniValue: this._styleMiniValue,
|
||||
},
|
||||
operations: {
|
||||
_restorePanelState: (...args) => this._restorePanelState(...args),
|
||||
@@ -586,7 +584,7 @@ export class StyleManager extends ShellFacade {
|
||||
this._initRecordingOverlay();
|
||||
this._startAnimationLoop();
|
||||
this._startTrafficChipTicker();
|
||||
this._updateStyleMiniStatus();
|
||||
this._publishActiveStyle();
|
||||
this._updateLocationMiniStatus();
|
||||
|
||||
this._shareRestoration.start();
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// src/ui/commandDock.test.mjs — the command dock and the scene chrome render
|
||||
// what VisualSettings publishes, over happy-dom.
|
||||
import '../testSupport/registerDom.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createPinia } from 'pinia';
|
||||
import { createApp, h, nextTick } from 'vue';
|
||||
import { setLocale } from '../i18n/index.js';
|
||||
import { useDisplayStore } from './stores/display.js';
|
||||
import CommandDockView from './views/CommandDockView.vue';
|
||||
import SceneChromeView from './views/SceneChromeView.vue';
|
||||
import { VisualSettings } from './visualSettings.js';
|
||||
|
||||
function fixture(t) {
|
||||
document.body.replaceChildren();
|
||||
const host = document.body.appendChild(document.createElement('div'));
|
||||
const pinia = createPinia();
|
||||
const app = createApp({
|
||||
render: () => [h(SceneChromeView), h(CommandDockView)],
|
||||
}).use(pinia);
|
||||
app.mount(host);
|
||||
t.after(() => app.unmount());
|
||||
const store = useDisplayStore(pinia);
|
||||
// VisualSettings publishes through its store; the method needs no more.
|
||||
const settings = Object.assign(Object.create(VisualSettings.prototype), {
|
||||
store,
|
||||
activeStyle: 'normal',
|
||||
});
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
const active = () =>
|
||||
[...document.querySelectorAll('.style-btn.active')].map(
|
||||
(button) => button.dataset.style,
|
||||
);
|
||||
return { $, settings, active };
|
||||
}
|
||||
|
||||
test('before VisualSettings publishes, the dock shows NORMAL', (t) => {
|
||||
const { $, active } = fixture(t);
|
||||
assert.deepEqual(active(), ['normal']);
|
||||
assert.equal($('#style-mini-value').textContent, 'NORMAL');
|
||||
assert.equal($('#active-style-name').textContent, 'NORMAL');
|
||||
});
|
||||
|
||||
test('the active style marks its button and labels the dock and the corner', async (t) => {
|
||||
const { $, settings, active } = fixture(t);
|
||||
settings._publishActiveStyle('thermal');
|
||||
await nextTick();
|
||||
assert.deepEqual(active(), ['thermal']);
|
||||
assert.equal($('#style-mini-value').textContent, 'FLIR');
|
||||
assert.equal($('#active-style-name').textContent, 'FLIR');
|
||||
settings.activeStyle = 'unlisted';
|
||||
settings._publishActiveStyle();
|
||||
await nextTick();
|
||||
assert.deepEqual(active(), []);
|
||||
assert.equal($('#style-mini-value').textContent, 'UNLISTED');
|
||||
settings._publishActiveStyle('normal');
|
||||
await nextTick();
|
||||
assert.deepEqual(active(), ['normal']);
|
||||
});
|
||||
|
||||
test('the style buttons keep classes other code sets', async (t) => {
|
||||
const { $, settings } = fixture(t);
|
||||
const noir = $('.style-btn[data-style="noir"]');
|
||||
noir.classList.add('voice-highlight');
|
||||
settings._publishActiveStyle('noir');
|
||||
await nextTick();
|
||||
assert.equal(noir.classList.contains('voice-highlight'), true);
|
||||
assert.equal(noir.classList.contains('active'), true);
|
||||
});
|
||||
|
||||
test('the dock and the corner label the style in the UI language', async (t) => {
|
||||
t.after(() => setLocale('en'));
|
||||
setLocale('ru');
|
||||
const { $, settings } = fixture(t);
|
||||
assert.equal($('#style-mini-value').textContent, 'ОБЫЧНЫЙ');
|
||||
assert.equal($('#active-style-name').textContent, 'ОБЫЧНЫЙ');
|
||||
settings._publishActiveStyle('noir');
|
||||
await nextTick();
|
||||
assert.equal($('#style-mini-value').textContent, 'НУАР');
|
||||
assert.equal($('#active-style-name').textContent, 'НУАР');
|
||||
});
|
||||
@@ -77,7 +77,6 @@ function styleOwner(t, initialVariant = 'cyber') {
|
||||
),
|
||||
},
|
||||
services: { governorRequestRender() {}, setDetectionStyle() {} },
|
||||
_styleIndicator: {},
|
||||
_setBloomEnabled: (value) => calls.push(['bloom', value]),
|
||||
_setSharpenEnabled: (value) => calls.push(['sharpen', value]),
|
||||
_applyDetectionPreset: (value) => calls.push(['detection', value]),
|
||||
@@ -86,7 +85,7 @@ function styleOwner(t, initialVariant = 'cyber') {
|
||||
for (const name of [
|
||||
'setCelestialRingEnabled',
|
||||
'_startTransition',
|
||||
'_updateStyleMiniStatus',
|
||||
'_publishActiveStyle',
|
||||
'_updateSliderPanel',
|
||||
'_updateHudButtonState',
|
||||
'_syncIrBoost',
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
export function readShellElements(document = globalThis.document) {
|
||||
return {
|
||||
_rightPanelStack: document.getElementById('right-context-rail'),
|
||||
_styleIndicator: document.getElementById('active-style-name'),
|
||||
_sliderPanel: document.getElementById('param-slider-panel'),
|
||||
_sliderContainer: document.getElementById('param-sliders'),
|
||||
_ppToggles: document.getElementById('pp-toggles'),
|
||||
@@ -132,7 +131,6 @@ export function readShellElements(document = globalThis.document) {
|
||||
_locationPills: document.getElementById('location-pills'),
|
||||
_poiRow: document.getElementById('poi-row'),
|
||||
_locationBarDivider: document.getElementById('location-bar-divider'),
|
||||
_styleMiniValue: document.getElementById('style-mini-value'),
|
||||
_locationMiniCity: document.getElementById('location-mini-city'),
|
||||
_locationMiniPoi: document.getElementById('location-mini-poi'),
|
||||
_safeFrameOverlay: document.getElementById('safe-frame-overlay'),
|
||||
|
||||
@@ -1304,8 +1304,8 @@ export class ShellFacade {
|
||||
* @param {string} [styleName=this.activeStyle] - Style name to display.
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateStyleMiniStatus(styleName = this.activeStyle) {
|
||||
return this._visualSettings._updateStyleMiniStatus(...arguments);
|
||||
_publishActiveStyle(styleName = this.activeStyle) {
|
||||
return this._visualSettings._publishActiveStyle(...arguments);
|
||||
}
|
||||
|
||||
_initOrbit(...args) {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { markRaw, shallowRef } from 'vue';
|
||||
* The Display panel as its view renders it. VisualSettings presents its
|
||||
* groups (`_presentDisplay()`), AircraftDisplay the 3D aircraft group, the
|
||||
* draw tool its controls and the file import its list, and each publishes
|
||||
* here; each keeps its listeners on the controls.
|
||||
* here; each keeps its listeners on the controls. VisualSettings also
|
||||
* publishes the active style, which the dock and the scene chrome show.
|
||||
*/
|
||||
export const useDisplayStore = defineStore('gev-display', () => {
|
||||
/** The presented panel; null until VisualSettings first renders. */
|
||||
@@ -40,6 +41,14 @@ export const useDisplayStore = defineStore('gev-display', () => {
|
||||
geoImport.value = markRaw(next);
|
||||
}
|
||||
|
||||
/** The active visual style; null until VisualSettings first presents it. */
|
||||
const style = shallowRef(null);
|
||||
|
||||
/** @param {{name: string, label: string}} next The active style and its label. */
|
||||
function publishStyle(next) {
|
||||
style.value = markRaw(next);
|
||||
}
|
||||
|
||||
return {
|
||||
view,
|
||||
publish,
|
||||
@@ -49,5 +58,7 @@ export const useDisplayStore = defineStore('gev-display', () => {
|
||||
publishDraw,
|
||||
geoImport,
|
||||
publishGeoImport,
|
||||
style,
|
||||
publishStyle,
|
||||
};
|
||||
});
|
||||
@@ -1,3 +1,14 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { vClassToggle } from '../rail/classToggle.js';
|
||||
import { useDisplayStore } from '../stores/display.js';
|
||||
|
||||
// VisualSettings publishes the active style; until it does, the dock shows
|
||||
// its initial markup. The style buttons keep their owner's listeners.
|
||||
const { style } = storeToRefs(useDisplayStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Unified bottom command dock: locations, voice (injected at runtime), visual presets -->
|
||||
<div id="command-dock" aria-label="Navigation, voice, and visual preset controls" data-i18n-aria-label>
|
||||
@@ -14,32 +25,32 @@
|
||||
<div id="control-panel-popover" class="dock-popover-content">
|
||||
<button class="dock-pin-btn" data-pin-target="control-panel" type="button" aria-label="Pin visual presets" aria-pressed="false" title="Keep visual presets open" data-i18n-aria-label data-i18n-title><img src="/pin.svg" alt="" /></button>
|
||||
<div id="style-buttons" class="button-grid">
|
||||
<button class="style-btn active" data-style="normal" title="Show the globe without a visual filter." data-i18n-title>
|
||||
<button class="style-btn active" data-style="normal" v-class-toggle="{ ...(style && { active: style.name === 'normal' }) }" title="Show the globe without a visual filter." data-i18n-title>
|
||||
<span class="btn-icon">◯</span>
|
||||
<span class="btn-label" data-i18n>Normal</span>
|
||||
<span class="btn-key">1</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="retro" title="Emulate a green phosphor CRT with scanlines and screen curvature." data-i18n-title>
|
||||
<button class="style-btn" data-style="retro" v-class-toggle="{ ...(style && { active: style.name === 'retro' }) }" title="Emulate a green phosphor CRT with scanlines and screen curvature." data-i18n-title>
|
||||
<span class="btn-icon">▦</span>
|
||||
<span class="btn-label" data-i18n>CRT</span>
|
||||
<span class="btn-key">2</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="surveillance" title="Simulate night-vision goggles with green intensification and a tube vignette." data-i18n-title>
|
||||
<button class="style-btn" data-style="surveillance" v-class-toggle="{ ...(style && { active: style.name === 'surveillance' }) }" title="Simulate night-vision goggles with green intensification and a tube vignette." data-i18n-title>
|
||||
<span class="btn-icon">🌙</span>
|
||||
<span class="btn-label" data-i18n>NVG</span>
|
||||
<span class="btn-key">3</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="thermal" title="Simulate FLIR-style thermal contrast. Turn up Ironbow for color." data-i18n-title>
|
||||
<button class="style-btn" data-style="thermal" v-class-toggle="{ ...(style && { active: style.name === 'thermal' }) }" title="Simulate FLIR-style thermal contrast. Turn up Ironbow for color." data-i18n-title>
|
||||
<span class="btn-icon">🌡️</span>
|
||||
<span class="btn-label" data-i18n>FLIR</span>
|
||||
<span class="btn-key">4</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="anime" title="Apply bright cel-shaded color and illustrated outlines." data-i18n-title>
|
||||
<button class="style-btn" data-style="anime" v-class-toggle="{ ...(style && { active: style.name === 'anime' }) }" title="Apply bright cel-shaded color and illustrated outlines." data-i18n-title>
|
||||
<span class="btn-icon">✦</span>
|
||||
<span class="btn-label" data-i18n>Anime</span>
|
||||
<span class="btn-key">5</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="noir" title="Apply high-contrast monochrome film-noir grading." data-i18n-title>
|
||||
<button class="style-btn" data-style="noir" v-class-toggle="{ ...(style && { active: style.name === 'noir' }) }" title="Apply high-contrast monochrome film-noir grading." data-i18n-title>
|
||||
<span class="btn-icon">◐</span>
|
||||
<span class="btn-label" data-i18n>Noir</span>
|
||||
<span class="btn-key">6</span>
|
||||
@@ -55,7 +66,7 @@
|
||||
</div>
|
||||
<div id="style-mini-status" class="panel-mini-status" aria-live="polite">
|
||||
<span class="panel-mini-label" data-i18n>Style</span>
|
||||
<span id="style-mini-value" class="panel-mini-value" data-i18n>NORMAL</span>
|
||||
<span id="style-mini-value" class="panel-mini-value">{{ style ? style.label : t('NORMAL') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { useDisplayStore } from '../stores/display.js';
|
||||
|
||||
// The ACTIVE STYLE corner shows the style VisualSettings publishes.
|
||||
const { style } = storeToRefs(useDisplayStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="cesiumContainer"></div>
|
||||
<div id="world-overlay-root" aria-hidden="true">
|
||||
@@ -18,7 +27,7 @@
|
||||
<!-- Style indicator -->
|
||||
<div id="style-indicator">
|
||||
<span class="indicator-label scene-text-holdout" data-i18n>ACTIVE STYLE</span>
|
||||
<span class="indicator-value scene-text-holdout" id="active-style-name" data-i18n>NORMAL</span>
|
||||
<span class="indicator-value scene-text-holdout" id="active-style-name">{{ style ? style.label : t('NORMAL') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Top-center camera and share actions -->
|
||||
|
||||
+11
-15
@@ -1517,16 +1517,8 @@ export class VisualSettings {
|
||||
this._applyStylePresetDefaults(styleName);
|
||||
}
|
||||
|
||||
// Update button UI
|
||||
document.querySelectorAll('.style-btn').forEach((btn) => {
|
||||
btn.classList.toggle('active', btn.dataset.style === styleName);
|
||||
});
|
||||
|
||||
// Update style indicator
|
||||
this._styleIndicator.textContent = STYLE_STATUS_LABELS[styleName]
|
||||
? t(STYLE_STATUS_LABELS[styleName])
|
||||
: styleName.toUpperCase();
|
||||
this._updateStyleMiniStatus(styleName);
|
||||
// The style buttons, the dock's mini status and the ACTIVE STYLE corner.
|
||||
this._publishActiveStyle(styleName);
|
||||
|
||||
// Update parameter sliders
|
||||
this._updateSliderPanel(styleName, { reveal: revealParameters });
|
||||
@@ -1555,11 +1547,15 @@ export class VisualSettings {
|
||||
this._visualEffects.startTransition(styleName, fromValue, toValue);
|
||||
}
|
||||
|
||||
_updateStyleMiniStatus(styleName = this.activeStyle) {
|
||||
if (!this._styleMiniValue) return;
|
||||
this._styleMiniValue.textContent = STYLE_STATUS_LABELS[styleName]
|
||||
? t(STYLE_STATUS_LABELS[styleName])
|
||||
: String(styleName || 'normal').toUpperCase();
|
||||
/** Publish the active style and its label in the UI language. */
|
||||
_publishActiveStyle(styleName = this.activeStyle) {
|
||||
const name = String(styleName || 'normal');
|
||||
this.store.publishStyle({
|
||||
name,
|
||||
label: STYLE_STATUS_LABELS[name]
|
||||
? t(STYLE_STATUS_LABELS[name])
|
||||
: name.toUpperCase(),
|
||||
});
|
||||
}
|
||||
|
||||
_updateHudButtonState() {
|
||||
|
||||
Reference in new issue
Block a user