mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +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
|
through the refresh their own click triggers, and clicks re-read the live
|
||||||
descriptor before acting), Display (in progress: `DisplayPanel` renders
|
descriptor before acting), Display (in progress: `DisplayPanel` renders
|
||||||
the HUD, Sonar, Scope, Celestial, detection, bloom and sharpen from the
|
the HUD, Sonar, Scope, Celestial, detection, bloom and sharpen from the
|
||||||
`gev-display` store, which VisualSettings publishes to; the other groups
|
`gev-display` store, which VisualSettings publishes to, and the 3D
|
||||||
follow one owner at a time), Context and Radio, the command dock,
|
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
|
the phone shell, then Cockpit. Layers that write into the right rail
|
||||||
directly gain a slot instead.
|
directly gain a slot instead.
|
||||||
|
|
||||||
|
|||||||
@@ -612,6 +612,7 @@ async function main() {
|
|||||||
if (models3dButton) {
|
if (models3dButton) {
|
||||||
if (!models3dButton.classList.contains('active')) models3dButton.click();
|
if (!models3dButton.classList.contains('active')) models3dButton.click();
|
||||||
models3dButton.click(); // → OFF: both layers must follow the one control
|
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.button = models3dButton.classList.contains('active');
|
||||||
models3dOff.fl = fl.getParams().models3d;
|
models3dOff.fl = fl.getParams().models3d;
|
||||||
models3dOff.mil = mil.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(
|
assert.match(
|
||||||
ui,
|
ui,
|
||||||
/this\._models3dBtn\?\.setAttribute\(\s*'aria-pressed',\s*String\(this\.(?:flightState\.)?_models3dEnabled\),?\s*\)/,
|
/this\._models3dPressedShown = this\._models3dEnabled;/,
|
||||||
'ui.js: and keeps aria-pressed synchronized, so the lit state is not colour-only',
|
'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(
|
assert.match(
|
||||||
html,
|
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. */
|
/** Own the shared aircraft display preference and its controls. */
|
||||||
export class AircraftDisplay {
|
export class AircraftDisplay {
|
||||||
constructor({ elements, readDataManager, layout }) {
|
constructor({
|
||||||
|
elements,
|
||||||
|
readDataManager,
|
||||||
|
layout,
|
||||||
|
store = useDisplayStore(applicationPinia()),
|
||||||
|
}) {
|
||||||
Object.assign(this, elements, {
|
Object.assign(this, elements, {
|
||||||
readDataManager,
|
readDataManager,
|
||||||
|
store,
|
||||||
_layoutRightPanels: layout,
|
_layoutRightPanels: layout,
|
||||||
});
|
});
|
||||||
// Both aircraft layers default to models enabled in proximity mode.
|
// Both aircraft layers default to models enabled in proximity mode.
|
||||||
this._models3dEnabled = true;
|
this._models3dEnabled = true;
|
||||||
this._models3dMode = 'proximity';
|
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 = [
|
this._models3dModeBtns = [
|
||||||
document.getElementById('models3d-mode-proximity'),
|
document.getElementById('models3d-mode-proximity'),
|
||||||
document.getElementById('models3d-mode-all'),
|
document.getElementById('models3d-mode-all'),
|
||||||
@@ -28,20 +41,28 @@ export class AircraftDisplay {
|
|||||||
if (!options) return;
|
if (!options) return;
|
||||||
this._models3dEnabled = options.models3d === true;
|
this._models3dEnabled = options.models3d === true;
|
||||||
this._models3dMode = options.models3dMode === 'all' ? 'all' : 'proximity';
|
this._models3dMode = options.models3dMode === 'all' ? 'all' : 'proximity';
|
||||||
this._syncModels3dButtonState();
|
this._models3dPressedShown = this._models3dEnabled;
|
||||||
this._models3dModeRow?.classList.toggle('visible', this._models3dEnabled);
|
this._models3dRowShown = this._models3dEnabled;
|
||||||
syncSegmentedControl(this._models3dModeBtns, this._models3dMode, {
|
this._models3dModeShown = this._models3dMode;
|
||||||
valueOf: (button) => button.dataset.mode,
|
this._publishModels3d();
|
||||||
});
|
|
||||||
this._layoutRightPanels();
|
this._layoutRightPanels();
|
||||||
}
|
}
|
||||||
|
|
||||||
_syncModels3dModeRow() {
|
_syncModels3dModeRow() {
|
||||||
if (this._models3dModeRow)
|
this._models3dRowShown = this._models3dEnabled;
|
||||||
this._models3dModeRow.classList.toggle('visible', this._models3dEnabled);
|
this._publishModels3d();
|
||||||
this._layoutRightPanels();
|
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() {
|
_initModels3dToggle() {
|
||||||
if (!this._models3dBtn) return;
|
if (!this._models3dBtn) return;
|
||||||
this._syncModels3dButtonState();
|
this._syncModels3dButtonState();
|
||||||
@@ -58,21 +79,17 @@ export class AircraftDisplay {
|
|||||||
const normalized = mode === 'all' ? 'all' : 'proximity';
|
const normalized = mode === 'all' ? 'all' : 'proximity';
|
||||||
this._models3dMode = normalized;
|
this._models3dMode = normalized;
|
||||||
this._setModels3dParams({ models3dMode: normalized });
|
this._setModels3dParams({ models3dMode: normalized });
|
||||||
syncSegmentedControl(this._models3dModeBtns, normalized, {
|
this._models3dModeShown = normalized;
|
||||||
valueOf: (button) => button.dataset.mode,
|
|
||||||
});
|
|
||||||
this._syncModels3dButtonState();
|
this._syncModels3dButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
_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
|
// 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
|
// a screen reader without aria-pressed, which the view renders with it. It
|
||||||
// ACTIVE from markup (default-on, 2026-08-22): the very first thing assistive
|
// matters more now that the button ships ACTIVE from markup (default-on,
|
||||||
// tech reported was an unpressed-looking control over an armed layer.
|
// 2026-08-22): the very first thing assistive tech reported was an
|
||||||
this._models3dBtn?.setAttribute(
|
// unpressed-looking control over an armed layer.
|
||||||
'aria-pressed',
|
this._models3dPressedShown = this._models3dEnabled;
|
||||||
String(this._models3dEnabled),
|
this._publishModels3d();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,7 +447,6 @@ export class StyleManager extends ShellFacade {
|
|||||||
this._aircraftDisplay = new AircraftDisplay({
|
this._aircraftDisplay = new AircraftDisplay({
|
||||||
elements: {
|
elements: {
|
||||||
_models3dBtn: this._models3dBtn,
|
_models3dBtn: this._models3dBtn,
|
||||||
_models3dModeRow: this._models3dModeRow,
|
|
||||||
},
|
},
|
||||||
readDataManager: () => this._dataManager,
|
readDataManager: () => this._dataManager,
|
||||||
layout: () => this._layoutRightPanels(),
|
layout: () => this._layoutRightPanels(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { createPinia } from 'pinia';
|
|||||||
import { createApp, nextTick } from 'vue';
|
import { createApp, nextTick } from 'vue';
|
||||||
import { useDisplayStore } from './stores/display.js';
|
import { useDisplayStore } from './stores/display.js';
|
||||||
import DisplayPanel from './views/panels/DisplayPanel.vue';
|
import DisplayPanel from './views/panels/DisplayPanel.vue';
|
||||||
|
import { AircraftDisplay } from './aircraftDisplay.js';
|
||||||
import { VisualSettings } from './visualSettings.js';
|
import { VisualSettings } from './visualSettings.js';
|
||||||
|
|
||||||
function fixture(t) {
|
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.classList.contains('active'), true);
|
||||||
assert.equal(celestial.title, 'Celestial ring — reveal the full globe');
|
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'),
|
_safeFrameBox: document.getElementById('safe-frame-box'),
|
||||||
_detectionBtn: document.getElementById('detection-toggle'),
|
_detectionBtn: document.getElementById('detection-toggle'),
|
||||||
_models3dBtn: document.getElementById('models3d-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';
|
import { markRaw, shallowRef } from 'vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Display panel as its view renders it. VisualSettings presents the
|
* The Display panel as its view renders it. VisualSettings presents its
|
||||||
* post-processing groups (`_presentDisplay()`) and publishes them here;
|
* groups (`_presentDisplay()`) and AircraftDisplay the 3D aircraft group,
|
||||||
* it keeps its listeners on the panel's controls.
|
* and each publishes here; both keep their listeners on the controls.
|
||||||
*/
|
*/
|
||||||
export const useDisplayStore = defineStore('gev-display', () => {
|
export const useDisplayStore = defineStore('gev-display', () => {
|
||||||
/** The presented panel; null until VisualSettings first renders. */
|
/** The presented panel; null until VisualSettings first renders. */
|
||||||
@@ -15,5 +15,13 @@ export const useDisplayStore = defineStore('gev-display', () => {
|
|||||||
view.value = markRaw(next);
|
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';
|
import { useDisplayStore } from '../../stores/display.js';
|
||||||
|
|
||||||
// The Display panel. VisualSettings presents the HUD, Sonar, Scope,
|
// The Display panel. VisualSettings presents the HUD, Sonar, Scope,
|
||||||
// Celestial, detection, bloom and sharpen and publishes them; until it does,
|
// Celestial, detection, bloom and sharpen and AircraftDisplay the 3D group,
|
||||||
// the panel shows its initial markup. The other groups keep their owners' element bindings.
|
// and each publishes them; until they do, the panel shows its initial
|
||||||
const { view } = storeToRefs(useDisplayStore());
|
// markup. The other groups keep their owners' element bindings.
|
||||||
|
const { view, models3d } = storeToRefs(useDisplayStore());
|
||||||
const pct = (value) => `${value}%`;
|
const pct = (value) => `${value}%`;
|
||||||
const hud = computed(() => view.value?.hud ?? null);
|
const hud = computed(() => view.value?.hud ?? null);
|
||||||
const sonar = computed(() => view.value?.sonar ?? 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 — available in Normal style')
|
||||||
: t('Celestial ring — reveal the full globe'),
|
: t('Celestial ring — reveal the full globe'),
|
||||||
);
|
);
|
||||||
const allocationSegment = (value) => ({
|
const models3dOn = computed(() => models3d.value?.pressed ?? true);
|
||||||
class: { active: allocation.value === value },
|
const modelsMode = computed(() => models3d.value?.mode ?? 'proximity');
|
||||||
'aria-checked': String(allocation.value === value),
|
// 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -125,15 +131,15 @@ const allocationSegment = (value) => ({
|
|||||||
button ships lit and the mode row ships open — the same markup-carries-the-default
|
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
|
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. -->
|
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-icon">✈</span>
|
||||||
<span class="pp-label">3D</span>
|
<span class="pp-label">3D</span>
|
||||||
</button>
|
</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>
|
<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>
|
<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-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" aria-checked="false" data-i18n>All</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user