mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
feat(ui): render the Display panel's 3D aircraft group from the store
This commit is contained in:
1 parent
1139d364c7
commit
926253e8a3
9 files changed
+126
-40
No files matched your search
@@ -95,8 +95,9 @@ They contain no application state.
|
||||
through the refresh their own click triggers, and clicks re-read the live
|
||||
descriptor before acting), Display (in progress: `DisplayPanel` renders
|
||||
the HUD, Sonar, Scope, Celestial, detection, bloom and sharpen from the
|
||||
`gev-display` store, which VisualSettings publishes to; the other groups
|
||||
follow one owner at a time), Context and Radio, the command dock,
|
||||
`gev-display` store, which VisualSettings publishes to, and the 3D
|
||||
aircraft group, which AircraftDisplay publishes beside it; the other
|
||||
groups follow one owner at a time), Context and Radio, the command dock,
|
||||
the phone shell, then Cockpit. Layers that write into the right rail
|
||||
directly gain a slot instead.
|
||||
|
||||
|
||||
@@ -612,6 +612,7 @@ async function main() {
|
||||
if (models3dButton) {
|
||||
if (!models3dButton.classList.contains('active')) models3dButton.click();
|
||||
models3dButton.click(); // → OFF: both layers must follow the one control
|
||||
await new Promise((resolve) => setTimeout(resolve)); // the view renders
|
||||
models3dOff.button = models3dButton.classList.contains('active');
|
||||
models3dOff.fl = fl.getParams().models3d;
|
||||
models3dOff.mil = mil.getParams().models3d;
|
||||
|
||||
@@ -744,8 +744,17 @@ test('a fresh boot starts 3D aircraft ON in proximity — codec, both layers, an
|
||||
);
|
||||
assert.match(
|
||||
ui,
|
||||
/this\._models3dBtn\?\.setAttribute\(\s*'aria-pressed',\s*String\(this\.(?:flightState\.)?_models3dEnabled\),?\s*\)/,
|
||||
'ui.js: and keeps aria-pressed synchronized, so the lit state is not colour-only',
|
||||
/this\._models3dPressedShown = this\._models3dEnabled;/,
|
||||
'ui.js: and keeps the pressed state synchronized, so the lit state is not colour-only',
|
||||
);
|
||||
const panel = await readFile(
|
||||
new URL('../ui/views/panels/DisplayPanel.vue', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(
|
||||
panel,
|
||||
/id="models3d-toggle" :aria-pressed="String\(models3dOn\)"/,
|
||||
'DisplayPanel.vue: the view renders aria-pressed with the lit state',
|
||||
);
|
||||
assert.match(
|
||||
html,
|
||||
|
||||
+37
-20
@@ -1,15 +1,28 @@
|
||||
import { syncSegmentedControl } from './segmentedControl.js';
|
||||
import { useDisplayStore } from './stores/display.js';
|
||||
import { applicationPinia } from './vueRuntime.js';
|
||||
|
||||
/** Own the shared aircraft display preference and its controls. */
|
||||
export class AircraftDisplay {
|
||||
constructor({ elements, readDataManager, layout }) {
|
||||
constructor({
|
||||
elements,
|
||||
readDataManager,
|
||||
layout,
|
||||
store = useDisplayStore(applicationPinia()),
|
||||
}) {
|
||||
Object.assign(this, elements, {
|
||||
readDataManager,
|
||||
store,
|
||||
_layoutRightPanels: layout,
|
||||
});
|
||||
// Both aircraft layers default to models enabled in proximity mode.
|
||||
this._models3dEnabled = true;
|
||||
this._models3dMode = 'proximity';
|
||||
// What the button, mode row and mode segments show; null until first
|
||||
// synced, so the initial markup stands. Enabling from the button leaves
|
||||
// the row to its own sync, so each is held apart.
|
||||
this._models3dPressedShown = null;
|
||||
this._models3dRowShown = null;
|
||||
this._models3dModeShown = null;
|
||||
this._models3dModeBtns = [
|
||||
document.getElementById('models3d-mode-proximity'),
|
||||
document.getElementById('models3d-mode-all'),
|
||||
@@ -28,20 +41,28 @@ export class AircraftDisplay {
|
||||
if (!options) return;
|
||||
this._models3dEnabled = options.models3d === true;
|
||||
this._models3dMode = options.models3dMode === 'all' ? 'all' : 'proximity';
|
||||
this._syncModels3dButtonState();
|
||||
this._models3dModeRow?.classList.toggle('visible', this._models3dEnabled);
|
||||
syncSegmentedControl(this._models3dModeBtns, this._models3dMode, {
|
||||
valueOf: (button) => button.dataset.mode,
|
||||
});
|
||||
this._models3dPressedShown = this._models3dEnabled;
|
||||
this._models3dRowShown = this._models3dEnabled;
|
||||
this._models3dModeShown = this._models3dMode;
|
||||
this._publishModels3d();
|
||||
this._layoutRightPanels();
|
||||
}
|
||||
|
||||
_syncModels3dModeRow() {
|
||||
if (this._models3dModeRow)
|
||||
this._models3dModeRow.classList.toggle('visible', this._models3dEnabled);
|
||||
this._models3dRowShown = this._models3dEnabled;
|
||||
this._publishModels3d();
|
||||
this._layoutRightPanels();
|
||||
}
|
||||
|
||||
/** Present the 3D aircraft group as its controls show it. */
|
||||
_publishModels3d() {
|
||||
this.store.publishModels3d({
|
||||
pressed: this._models3dPressedShown,
|
||||
rowVisible: this._models3dRowShown,
|
||||
mode: this._models3dModeShown,
|
||||
});
|
||||
}
|
||||
|
||||
_initModels3dToggle() {
|
||||
if (!this._models3dBtn) return;
|
||||
this._syncModels3dButtonState();
|
||||
@@ -58,21 +79,17 @@ export class AircraftDisplay {
|
||||
const normalized = mode === 'all' ? 'all' : 'proximity';
|
||||
this._models3dMode = normalized;
|
||||
this._setModels3dParams({ models3dMode: normalized });
|
||||
syncSegmentedControl(this._models3dModeBtns, normalized, {
|
||||
valueOf: (button) => button.dataset.mode,
|
||||
});
|
||||
this._models3dModeShown = normalized;
|
||||
this._syncModels3dButtonState();
|
||||
}
|
||||
|
||||
_syncModels3dButtonState() {
|
||||
this._models3dBtn?.classList.toggle('active', this._models3dEnabled);
|
||||
// The lit/dark state is a colour to a sighted operator and nothing at all to
|
||||
// a screen reader without this. It matters more now that the button ships
|
||||
// ACTIVE from markup (default-on, 2026-08-22): the very first thing assistive
|
||||
// tech reported was an unpressed-looking control over an armed layer.
|
||||
this._models3dBtn?.setAttribute(
|
||||
'aria-pressed',
|
||||
String(this._models3dEnabled),
|
||||
);
|
||||
// a screen reader without aria-pressed, which the view renders with it. It
|
||||
// matters more now that the button ships ACTIVE from markup (default-on,
|
||||
// 2026-08-22): the very first thing assistive tech reported was an
|
||||
// unpressed-looking control over an armed layer.
|
||||
this._models3dPressedShown = this._models3dEnabled;
|
||||
this._publishModels3d();
|
||||
}
|
||||
}
|
||||
@@ -447,7 +447,6 @@ export class StyleManager extends ShellFacade {
|
||||
this._aircraftDisplay = new AircraftDisplay({
|
||||
elements: {
|
||||
_models3dBtn: this._models3dBtn,
|
||||
_models3dModeRow: this._models3dModeRow,
|
||||
},
|
||||
readDataManager: () => this._dataManager,
|
||||
layout: () => this._layoutRightPanels(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createPinia } from 'pinia';
|
||||
import { createApp, nextTick } from 'vue';
|
||||
import { useDisplayStore } from './stores/display.js';
|
||||
import DisplayPanel from './views/panels/DisplayPanel.vue';
|
||||
import { AircraftDisplay } from './aircraftDisplay.js';
|
||||
import { VisualSettings } from './visualSettings.js';
|
||||
|
||||
function fixture(t) {
|
||||
@@ -297,3 +298,48 @@ test('Scope and Celestial render what VisualSettings shows', async (t) => {
|
||||
assert.equal(celestial.classList.contains('active'), true);
|
||||
assert.equal(celestial.title, 'Celestial ring — reveal the full globe');
|
||||
});
|
||||
|
||||
test('the 3D aircraft group renders what AircraftDisplay shows', async (t) => {
|
||||
const { store, $ } = fixture(t);
|
||||
const params = [];
|
||||
const owner = new AircraftDisplay({
|
||||
elements: { _models3dBtn: $('models3d-toggle') },
|
||||
readDataManager: () => ({
|
||||
setLayerParams: (id, next) => params.push([id, next]),
|
||||
}),
|
||||
layout() {},
|
||||
store,
|
||||
});
|
||||
const button = $('models3d-toggle');
|
||||
const row = $('models3d-mode-row');
|
||||
const all = $('models3d-mode-all');
|
||||
assert.equal(button.className, 'pp-toggle-btn active');
|
||||
assert.equal(button.getAttribute('aria-pressed'), 'true');
|
||||
assert.equal(row.classList.contains('visible'), true);
|
||||
assert.equal($('models3d-mode-proximity').getAttribute('aria-checked'), 'true');
|
||||
|
||||
// The button leaves the row to its own sync.
|
||||
owner._setModels3dEnabled(false);
|
||||
await nextTick();
|
||||
assert.equal(button.className, 'pp-toggle-btn');
|
||||
assert.equal(button.getAttribute('aria-pressed'), 'false');
|
||||
assert.equal(row.classList.contains('visible'), true);
|
||||
owner._syncModels3dModeRow();
|
||||
await nextTick();
|
||||
assert.equal(row.classList.contains('visible'), false);
|
||||
|
||||
owner._setModels3dMode('all');
|
||||
await nextTick();
|
||||
assert.equal(all.classList.contains('active'), true);
|
||||
assert.equal(all.getAttribute('aria-checked'), 'true');
|
||||
assert.equal($('models3d-mode-proximity').classList.contains('active'), false);
|
||||
assert.deepEqual(params.at(-1), ['military', { models3dMode: 'all' }]);
|
||||
|
||||
owner._syncModels3dFromLayerState({
|
||||
options: { flights: { models3d: true, models3dMode: 'proximity' } },
|
||||
});
|
||||
await nextTick();
|
||||
assert.equal(button.getAttribute('aria-pressed'), 'true');
|
||||
assert.equal(row.classList.contains('visible'), true);
|
||||
assert.equal(all.getAttribute('aria-checked'), 'false');
|
||||
});
|
||||
@@ -142,6 +142,5 @@ export function readShellElements(document = globalThis.document) {
|
||||
_safeFrameBox: document.getElementById('safe-frame-box'),
|
||||
_detectionBtn: document.getElementById('detection-toggle'),
|
||||
_models3dBtn: document.getElementById('models3d-toggle'),
|
||||
_models3dModeRow: document.getElementById('models3d-mode-row'),
|
||||
};
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { defineStore } from 'pinia';
|
||||
import { markRaw, shallowRef } from 'vue';
|
||||
|
||||
/**
|
||||
* The Display panel as its view renders it. VisualSettings presents the
|
||||
* post-processing groups (`_presentDisplay()`) and publishes them here;
|
||||
* it keeps its listeners on the panel's controls.
|
||||
* The Display panel as its view renders it. VisualSettings presents its
|
||||
* groups (`_presentDisplay()`) and AircraftDisplay the 3D aircraft group,
|
||||
* and each publishes here; both keep their listeners on the controls.
|
||||
*/
|
||||
export const useDisplayStore = defineStore('gev-display', () => {
|
||||
/** The presented panel; null until VisualSettings first renders. */
|
||||
@@ -15,5 +15,13 @@ export const useDisplayStore = defineStore('gev-display', () => {
|
||||
view.value = markRaw(next);
|
||||
}
|
||||
|
||||
return { view, publish };
|
||||
/** The 3D aircraft group; null until AircraftDisplay first renders. */
|
||||
const models3d = shallowRef(null);
|
||||
|
||||
/** @param {object} next A fresh presentation of the 3D aircraft group. */
|
||||
function publishModels3d(next) {
|
||||
models3d.value = markRaw(next);
|
||||
}
|
||||
|
||||
return { view, publish, models3d, publishModels3d };
|
||||
});
|
||||
@@ -6,9 +6,10 @@ import { vRangeValue } from '../../rail/rangeValue.js';
|
||||
import { useDisplayStore } from '../../stores/display.js';
|
||||
|
||||
// The Display panel. VisualSettings presents the HUD, Sonar, Scope,
|
||||
// Celestial, detection, bloom and sharpen and publishes them; until it does,
|
||||
// the panel shows its initial markup. The other groups keep their owners' element bindings.
|
||||
const { view } = storeToRefs(useDisplayStore());
|
||||
// Celestial, detection, bloom and sharpen and AircraftDisplay the 3D group,
|
||||
// and each publishes them; until they do, the panel shows its initial
|
||||
// markup. The other groups keep their owners' element bindings.
|
||||
const { view, models3d } = storeToRefs(useDisplayStore());
|
||||
const pct = (value) => `${value}%`;
|
||||
const hud = computed(() => view.value?.hud ?? null);
|
||||
const sonar = computed(() => view.value?.sonar ?? null);
|
||||
@@ -24,10 +25,15 @@ const celestialTitle = computed(() =>
|
||||
? t('Celestial ring — available in Normal style')
|
||||
: t('Celestial ring — reveal the full globe'),
|
||||
);
|
||||
const allocationSegment = (value) => ({
|
||||
class: { active: allocation.value === value },
|
||||
'aria-checked': String(allocation.value === value),
|
||||
const models3dOn = computed(() => models3d.value?.pressed ?? true);
|
||||
const modelsMode = computed(() => models3d.value?.mode ?? 'proximity');
|
||||
// A segmented control's segment, selected when `current` holds its value.
|
||||
const segment = (current) => (value) => ({
|
||||
class: { active: current.value === value },
|
||||
'aria-checked': String(current.value === value),
|
||||
});
|
||||
const modelsSegment = segment(modelsMode);
|
||||
const allocationSegment = segment(allocation);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -125,15 +131,15 @@ const allocationSegment = (value) => ({
|
||||
button ships lit and the mode row ships open — the same markup-carries-the-default
|
||||
convention as #scope-toggle. ui.js re-syncs both from durable layer state, which
|
||||
is what flips them back for a share link or a stored session carrying 3D off. -->
|
||||
<button class="pp-toggle-btn active" id="models3d-toggle" aria-pressed="true" title="3D aircraft — flat icons zoomed out, 3D models up close" data-i18n-title>
|
||||
<button class="pp-toggle-btn" :class="{ active: models3dOn }" id="models3d-toggle" :aria-pressed="String(models3dOn)" title="3D aircraft — flat icons zoomed out, 3D models up close" data-i18n-title>
|
||||
<span class="pp-icon">✈</span>
|
||||
<span class="pp-label">3D</span>
|
||||
</button>
|
||||
<div class="pp-slider-row visible" id="models3d-mode-row">
|
||||
<div class="pp-slider-row" :class="{ visible: models3d?.rowVisible ?? true }" id="models3d-mode-row">
|
||||
<span class="pp-slider-mini-label" data-i18n>Models</span>
|
||||
<div class="gev-segmented pp-mode-seg" role="radiogroup" aria-label="3D model coverage" data-i18n-aria-label>
|
||||
<button class="gev-segment pp-mode-btn active" id="models3d-mode-proximity" data-mode="proximity" role="radio" aria-checked="true" data-i18n>Proximity</button>
|
||||
<button class="gev-segment pp-mode-btn" id="models3d-mode-all" data-mode="all" role="radio" aria-checked="false" data-i18n>All</button>
|
||||
<button class="gev-segment pp-mode-btn" id="models3d-mode-proximity" data-mode="proximity" role="radio" v-bind="modelsSegment('proximity')" data-i18n>Proximity</button>
|
||||
<button class="gev-segment pp-mode-btn" id="models3d-mode-all" data-mode="all" role="radio" v-bind="modelsSegment('all')" data-i18n>All</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user