feat(ui): render the Display panel's bloom and sharpen from a store

This commit is contained in:
Sameh Khamis committed 2026-10-30 22:41:07 -07:00
1 parent 5707a8a151
commit 343bdebe98
18 files changed
+453 -311

No files matched your search

+4 -1
View File
@@ -93,7 +93,10 @@ They contain no application state.
live video surface), Layers (done: `LayerPanel` presents each row and live video surface), Layers (done: `LayerPanel` presents each row and
renders `LayerRows` synchronously; keyed chips and list items keep focus renders `LayerRows` synchronously; keyed chips and list items keep focus
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, Context and Radio, the command dock, descriptor before acting), Display (in progress: `DisplayPanel` renders
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,
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.
+11 -4
View File
@@ -51,7 +51,10 @@
"src/ui/cctv/CctvPicker.vue", "src/ui/cctv/CctvPicker.vue",
"src/ui/stores/cctv.js", "src/ui/stores/cctv.js",
"src/ui/stores/streetLevel.js", "src/ui/stores/streetLevel.js",
"src/ui/rail/RailChips.vue" "src/ui/rail/RailChips.vue",
"src/ui/views/panels/DisplayPanel.vue",
"src/ui/stores/display.js",
"src/ui/rail/rangeValue.js"
], ],
"external": ["vue", "pinia"] "external": ["vue", "pinia"]
}, },
@@ -955,7 +958,8 @@
"src/ui/rail/rangeValue.js", "src/ui/rail/rangeValue.js",
"src/ui/weather/WeatherReadout.vue", "src/ui/weather/WeatherReadout.vue",
"src/ui/stores/streetLevel.js", "src/ui/stores/streetLevel.js",
"src/ui/stores/cctv.js" "src/ui/stores/cctv.js",
"src/ui/stores/display.js"
], ],
"external": ["cesium", "vue", "pinia"] "external": ["cesium", "vue", "pinia"]
}, },
@@ -2922,7 +2926,8 @@
"src/ui/recentImagery/RecentImageryCard.vue", "src/ui/recentImagery/RecentImageryCard.vue",
"src/ui/recentImagery/RecentImageryReadout.vue", "src/ui/recentImagery/RecentImageryReadout.vue",
"src/ui/stores/streetLevel.js", "src/ui/stores/streetLevel.js",
"src/ui/stores/cctv.js" "src/ui/stores/cctv.js",
"src/ui/stores/display.js"
], ],
"external": [ "external": [
"hls.js", "hls.js",
@@ -3670,6 +3675,8 @@
"src/ui/visualInput.js", "src/ui/visualInput.js",
"src/ui/visualPresets.js", "src/ui/visualPresets.js",
"src/ui/visualSettings.js", "src/ui/visualSettings.js",
"src/ui/stores/display.js",
"src/ui/vueRuntime.js",
"src/ui/segmentedControl.js", "src/ui/segmentedControl.js",
"src/i18n/marker.js", "src/i18n/marker.js",
"src/i18n/index.js", "src/i18n/index.js",
@@ -3688,7 +3695,7 @@
"src/ui/displayControls.js", "src/ui/displayControls.js",
"src/visualStylePolicy.js" "src/visualStylePolicy.js"
], ],
"external": ["cesium"] "external": ["cesium", "vue", "pinia"]
}, },
"ui-panel-chrome": { "ui-panel-chrome": {
"exports": ["./ui/panel-chrome"], "exports": ["./ui/panel-chrome"],
+1 -1
View File
@@ -310,7 +310,7 @@ try {
countFrames(2_500), countFrames(2_500),
page.evaluate(() => new Promise((resolve) => setTimeout(() => { page.evaluate(() => new Promise((resolve) => setTimeout(() => {
// Drive the actual sharpen slider: input event → handler → // Drive the actual sharpen slider: input event → handler →
// _applySharpenIntensity → governorRequestRender. Proves the wiring, // _setSharpenIntensity → governorRequestRender. Proves the wiring,
// not just the facade. // not just the facade.
const slider = document.getElementById('sharpen-intensity-slider'); const slider = document.getElementById('sharpen-intensity-slider');
if (!slider) { resolve({ ok: false }); return; } if (!slider) { resolve({ ok: false }); return; }
+1 -1
View File
@@ -64,7 +64,7 @@ test('the engine resolves manual geometry before any name resolution', () => {
test('the Draw control markup carries the ids the tool binds to', () => { test('the Draw control markup carries the ids the tool binds to', () => {
// The DISPLAY rail is a Vue view now, rendered into index.html's view host — // The DISPLAY rail is a Vue view now, rendered into index.html's view host —
// the markup is not in index.html any more. // the markup is not in index.html any more.
const html = read('src/ui/views/DisplayControlsView.vue'); const html = read('src/ui/views/panels/DisplayPanel.vue');
const ids = [ const ids = [
'draw-toggle', 'draw-toggle',
'draw-mode-row', 'draw-mode-row',
+2 -2
View File
@@ -522,7 +522,7 @@ test('every explicit visual UI gesture claims restore authority before it mutate
['toggleSharpen:', 'setScopeEnabled:', 'this._setSharpenEnabled(', 'toggleSharpen control'], ['toggleSharpen:', 'setScopeEnabled:', 'this._setSharpenEnabled(', 'toggleSharpen control'],
['setScopeEnabled:', 'setScopeFeather:', 'setScopeMaskEnabled(', 'setScopeEnabled control'], ['setScopeEnabled:', 'setScopeFeather:', 'setScopeMaskEnabled(', 'setScopeEnabled control'],
['setScopeFeather:', 'setSharpenIntensity:', 'setScopeMaskFeather(', 'setScopeFeather control'], ['setScopeFeather:', 'setSharpenIntensity:', 'setScopeMaskFeather(', 'setScopeFeather control'],
['setSharpenIntensity:', 'setHudLayout:', 'this._applySharpenIntensity(', 'setSharpenIntensity control'], ['setSharpenIntensity:', 'setHudLayout:', 'this._setSharpenIntensity(', 'setSharpenIntensity control'],
['setHudLayout:', 'toggleCleanView:', 'this._setHudVariant(', 'setHudLayout control'], ['setHudLayout:', 'toggleCleanView:', 'this._setHudVariant(', 'setHudLayout control'],
['setDensity:', 'setAllocation:', 'this._applyDetectionDensityFromUi()', 'setDensity control'], ['setDensity:', 'setAllocation:', 'this._applyDetectionDensityFromUi()', 'setDensity control'],
['setAllocation:', 'setFade:', 'this._setDetectionAllocation(', 'setAllocation control'], ['setAllocation:', 'setFade:', 'this._setDetectionAllocation(', 'setAllocation control'],
@@ -632,7 +632,7 @@ test('every explicit visual control facade claims restore authority before mutat
[' setHudLayout(variantName) {', ' getDetectionState() {', 'this._setHudVariant(', 'setHudLayout'], [' setHudLayout(variantName) {', ' getDetectionState() {', 'this._setHudVariant(', 'setHudLayout'],
[' setDetection({ enabled, mode, densityPct, allocationStrategy, fadePct, outsideOpacityPct } = {}) {', ' async setMapStack(stackId) {', 'this._setDetectionAllocation(', 'setDetection'], [' setDetection({ enabled, mode, densityPct, allocationStrategy, fadePct, outsideOpacityPct } = {}) {', ' async setMapStack(stackId) {', 'this._setDetectionAllocation(', 'setDetection'],
[' setBloom({ enabled, intensityPct } = {}) {', ' setSharpen({ enabled, intensityPct } = {}) {', 'this._setBloomIntensity(', 'setBloom'], [' setBloom({ enabled, intensityPct } = {}) {', ' setSharpen({ enabled, intensityPct } = {}) {', 'this._setBloomIntensity(', 'setBloom'],
[' setSharpen({ enabled, intensityPct } = {}) {', ' get celestialRingEnabled() {', 'this._applySharpenIntensity(', 'setSharpen'], [' setSharpen({ enabled, intensityPct } = {}) {', ' get celestialRingEnabled() {', 'this._setSharpenIntensity(', 'setSharpen'],
[' setCelestialRingEnabled(enabled, { syncShare = true, focus = false } = {}) {', ' setOrbit(enabled) {', 'this.celestialRing?.setEnabled(', 'setCelestialRingEnabled'], [' setCelestialRingEnabled(enabled, { syncShare = true, focus = false } = {}) {', ' setOrbit(enabled) {', 'this.celestialRing?.setEnabled(', 'setCelestialRingEnabled'],
]; ];
for (const [start, end, mutation, label] of facadeRoutes) { for (const [start, end, mutation, label] of facadeRoutes) {
+1 -1
View File
@@ -208,7 +208,7 @@ test('DETECT uses the real OFF/restore button and density input to reach DENSE',
assert.equal(await detectOn(page, pause), 'DENSE'); assert.equal(await detectOn(page, pause), 'DENSE');
assert.deepEqual(modes, ['BALANCED', 'OFF', 'DENSE']); assert.deepEqual(modes, ['BALANCED', 'OFF', 'DENSE']);
const template = readFileSync( const template = readFileSync(
new URL('../ui/views/DisplayControlsView.vue', import.meta.url), new URL('../ui/views/panels/DisplayPanel.vue', import.meta.url),
'utf8', 'utf8',
); );
assert.match(template, /id="detection-density-slider"/); assert.match(template, /id="detection-density-slider"/);
+3 -17
View File
@@ -207,10 +207,6 @@ export class StyleManager extends ShellFacade {
setScopeMaskFeather: services.setScopeMaskFeather, setScopeMaskFeather: services.setScopeMaskFeather,
}, },
elements: { elements: {
_bloomBtn: this._bloomBtn,
_bloomSlider: this._bloomSlider,
_bloomSliderRow: this._bloomSliderRow,
_bloomSliderValue: this._bloomSliderValue,
_celestialBtn: this._celestialBtn, _celestialBtn: this._celestialBtn,
_cockpitDisplayToggleBtn: this._cockpitDisplayToggleBtn, _cockpitDisplayToggleBtn: this._cockpitDisplayToggleBtn,
_detectionAllocationRow: this._detectionAllocationRow, _detectionAllocationRow: this._detectionAllocationRow,
@@ -242,10 +238,6 @@ export class StyleManager extends ShellFacade {
_scopeBtn: this._scopeBtn, _scopeBtn: this._scopeBtn,
_scopeFeatherSlider: this._scopeFeatherSlider, _scopeFeatherSlider: this._scopeFeatherSlider,
_scopeFeatherValue: this._scopeFeatherValue, _scopeFeatherValue: this._scopeFeatherValue,
_sharpenBtn: this._sharpenBtn,
_sharpenSlider: this._sharpenSlider,
_sharpenSliderRow: this._sharpenSliderRow,
_sharpenSliderValue: this._sharpenSliderValue,
_sliderContainer: this._sliderContainer, _sliderContainer: this._sliderContainer,
_sliderPanel: this._sliderPanel, _sliderPanel: this._sliderPanel,
_styleIndicator: this._styleIndicator, _styleIndicator: this._styleIndicator,
@@ -552,7 +544,6 @@ export class StyleManager extends ShellFacade {
_celestialBtn: this._celestialBtn, _celestialBtn: this._celestialBtn,
_models3dBtn: this._models3dBtn, _models3dBtn: this._models3dBtn,
_scopeFeatherValue: this._scopeFeatherValue, _scopeFeatherValue: this._scopeFeatherValue,
_sharpenSliderValue: this._sharpenSliderValue,
_detectionDensityValue: this._detectionDensityValue, _detectionDensityValue: this._detectionDensityValue,
}, },
operations: { operations: {
@@ -565,8 +556,7 @@ export class StyleManager extends ShellFacade {
_setBloomEnabled: (...args) => this._setBloomEnabled(...args), _setBloomEnabled: (...args) => this._setBloomEnabled(...args),
_setBloomIntensity: (...args) => this._setBloomIntensity(...args), _setBloomIntensity: (...args) => this._setBloomIntensity(...args),
_setSharpenEnabled: (...args) => this._setSharpenEnabled(...args), _setSharpenEnabled: (...args) => this._setSharpenEnabled(...args),
_applySharpenIntensity: (...args) => _setSharpenIntensity: (...args) => this._setSharpenIntensity(...args),
this._applySharpenIntensity(...args),
_setHudVariant: (...args) => this._setHudVariant(...args), _setHudVariant: (...args) => this._setHudVariant(...args),
_setCyberSonarEnabled: (...args) => this._setCyberSonarEnabled(...args), _setCyberSonarEnabled: (...args) => this._setCyberSonarEnabled(...args),
_setCyberSonarSetting: (...args) => this._setCyberSonarSetting(...args), _setCyberSonarSetting: (...args) => this._setCyberSonarSetting(...args),
@@ -1484,15 +1474,11 @@ export class StyleManager extends ShellFacade {
detection: this.getDetectionState(), detection: this.getDetectionState(),
bloom: { bloom: {
enabled: !!this.bloomEnabled, enabled: !!this.bloomEnabled,
intensityPct: this._bloomSlider intensityPct: this._visualSettings.bloomIntensityPct,
? parseInt(this._bloomSlider.value, 10)
: null,
}, },
sharpen: { sharpen: {
enabled: !!this.sharpenEnabled, enabled: !!this.sharpenEnabled,
intensityPct: this._sharpenSlider intensityPct: this._visualSettings.sharpenIntensityPct,
? parseInt(this._sharpenSlider.value, 10)
: null,
}, },
celestialRing: { celestialRing: {
enabled: this.celestialRingEnabled, enabled: this.celestialRingEnabled,
+1 -1
View File
@@ -82,7 +82,7 @@ test('real runner and facade use visual settings, preserve visibility and return
}); });
test('native sonar sliders represent every accepted integer without browser snapping', () => { test('native sonar sliders represent every accepted integer without browser snapping', () => {
const html = readFileSync(new URL('./views/DisplayControlsView.vue', import.meta.url), 'utf8'); const html = readFileSync(new URL('./views/panels/DisplayPanel.vue', import.meta.url), 'utf8');
for (const name of ['rings', 'range', 'intensity', 'opacity', 'sector']) { for (const name of ['rings', 'range', 'intensity', 'opacity', 'sector']) {
assert.match(html, new RegExp(`id="cyber-sonar-${name}"[^>]*step="1"`)); assert.match(html, new RegExp(`id="cyber-sonar-${name}"[^>]*step="1"`));
} }
+5 -2
View File
@@ -94,6 +94,9 @@ function styleOwner(t, initialVariant = 'cyber') {
'_syncCyberSonarControl', '_syncCyberSonarControl',
'_scheduleAdaptivePanelLayout', '_scheduleAdaptivePanelLayout',
'_applyDetectionFadeFromUi', '_applyDetectionFadeFromUi',
// Intensities: the fixture has no post-process stages to drive.
'_setBloomIntensity',
'_setSharpenIntensity',
]) ])
owner[name] = () => {}; owner[name] = () => {};
return { owner, hud, root, calls }; return { owner, hud, root, calls };
@@ -647,7 +650,7 @@ test('Cyber visual defaults update the style, thermal palette and render state',
}); });
test('the Display selector, voice schema and final stylesheet expose Cyber', () => { test('the Display selector, voice schema and final stylesheet expose Cyber', () => {
const display = read('./views/DisplayControlsView.vue'); const display = read('./views/panels/DisplayPanel.vue');
const stylesheet = read('../../style.css'); const stylesheet = read('../../style.css');
const rightRail = read('./rightPanelRail.js'); const rightRail = read('./rightPanelRail.js');
const hud = read('../hud.js'); const hud = read('../hud.js');
@@ -877,7 +880,7 @@ test('Cyber panels retain clean edges without the added footer band', () => {
test('Cyber keeps Sonar on/off without the removed CSS effect switch or dimmer', () => { test('Cyber keeps Sonar on/off without the removed CSS effect switch or dimmer', () => {
const template = readFileSync( const template = readFileSync(
new URL('./views/DisplayControlsView.vue', import.meta.url), new URL('./views/panels/DisplayPanel.vue', import.meta.url),
'utf8', 'utf8',
); );
assert.match(template, /id="cyber-sonar-toggle"/); assert.match(template, /id="cyber-sonar-toggle"/);
+1 -4
View File
@@ -156,10 +156,7 @@ export class DisplayBindings {
}, },
setSharpenIntensity: (pct) => { setSharpenIntensity: (pct) => {
this.shareLinkManager?.claimRestoreLane?.('visual'); this.shareLinkManager?.claimRestoreLane?.('visual');
if (this._sharpenSliderValue) this._setSharpenIntensity(pct);
this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
this._syncShareState();
}, },
setHudLayout: (value) => { setHudLayout: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual'); this.shareLinkManager?.claimRestoreLane?.('visual');
+123
View File
@@ -0,0 +1,123 @@
// src/ui/displayPanel.test.mjs — the Display panel renders bloom and sharpen
// from what VisualSettings publishes, and shows its initial markup before.
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createPinia } from 'pinia';
import { createApp, nextTick } from 'vue';
import { useDisplayStore } from './stores/display.js';
import DisplayPanel from './views/panels/DisplayPanel.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(DisplayPanel).use(pinia);
app.mount(host);
t.after(() => app.unmount());
const $ = (id) => document.getElementById(id);
return { store: useDisplayStore(pinia), $ };
}
/** A VisualSettings over stand-in effects, publishing to `store`. */
function settings(store) {
const owner = Object.create(VisualSettings.prototype);
const effects = {
bloomEnabled: false,
sharpenEnabled: false,
bloomIntensity: 0,
sharpenIntensity: 0.49,
applyBloomIntensity(value) {
this.bloomIntensity = value;
},
applySharpenIntensity(value) {
this.sharpenIntensity = value;
},
setBloomEnabled(on) {
this.bloomEnabled = on;
},
setSharpenEnabled(on) {
this.sharpenEnabled = on;
},
syncBloomEnabled() {},
};
const shares = [];
Object.assign(owner, {
store,
_visualEffects: effects,
_sharpenPct: 49,
services: { governorRequestRender() {} },
_syncShareState: () => shares.push(true),
_layoutRightPanels() {},
});
return { owner, effects, shares };
}
test('before VisualSettings renders, the panel shows its initial markup', (t) => {
const { $ } = fixture(t);
assert.equal($('bloom-toggle').className, 'pp-toggle-btn');
assert.equal($('bloom-slider-row').className, 'pp-slider-row');
assert.equal($('bloom-intensity-slider').value, '0');
assert.equal($('bloom-intensity-value').textContent, '0%');
assert.equal($('sharpen-intensity-slider').value, '49');
assert.equal($('sharpen-intensity-value').textContent, '49%');
});
test('bloom and sharpen render their state, slider and readout', async (t) => {
const { store, $ } = fixture(t);
const { owner, effects, shares } = settings(store);
owner._setBloomEnabled(true);
owner._setBloomIntensity(140.4);
await nextTick();
assert.equal($('bloom-toggle').classList.contains('active'), true);
assert.equal($('bloom-slider-row').classList.contains('visible'), true);
assert.equal($('bloom-intensity-slider').value, '140');
assert.equal($('bloom-intensity-value').textContent, '140%');
assert.equal(effects.bloomIntensity, 140);
owner._setSharpenEnabled(true);
owner._setSharpenIntensity(130);
await nextTick();
assert.equal($('sharpen-toggle').classList.contains('active'), true);
assert.equal($('sharpen-slider-row').classList.contains('visible'), true);
assert.equal($('sharpen-intensity-slider').value, '100', 'clamped to 100');
assert.equal($('sharpen-intensity-value').textContent, '100%');
assert.equal(effects.sharpenIntensity, 1);
assert.equal(owner.sharpenIntensityPct, 100);
assert.ok(shares.length > 0, 'changes reach the share state');
owner._setBloomEnabled(false);
await nextTick();
assert.equal($('bloom-toggle').classList.contains('active'), false);
assert.equal($('bloom-slider-row').classList.contains('visible'), false);
assert.equal($('bloom-intensity-value').textContent, '140%', 'the amount stays');
});
test('a publish never rewrites a slider the user is dragging', async (t) => {
const { store, $ } = fixture(t);
const { owner } = settings(store);
owner._setSharpenIntensity(30);
await nextTick();
const slider = $('sharpen-intensity-slider');
let writes = 0;
const descriptor = Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(slider),
'value',
);
Object.defineProperty(slider, 'value', {
get: () => descriptor.get.call(slider),
set: (next) => {
writes++;
descriptor.set.call(slider, next);
},
});
// The input handler applies the dragged value; the publish that follows
// carries the same value, so the slider is left alone.
owner._setSharpenIntensity(30);
owner._setBloomEnabled(true);
await nextTick();
assert.equal(writes, 0);
assert.equal(slider.value, '30');
});
+1 -1
View File
@@ -439,7 +439,7 @@ test('the application composition wires the import and owns its teardown', () =>
); );
assert.ok(tools.indexOf('defer(() => geoFileImport?.destroy());') > importAt); assert.ok(tools.indexOf('defer(() => geoFileImport?.destroy());') > importAt);
const markup = fs.readFileSync( const markup = fs.readFileSync(
new URL('./views/DisplayControlsView.vue', import.meta.url), new URL('./views/panels/DisplayPanel.vue', import.meta.url),
'utf8', 'utf8',
); );
for (const id of [ for (const id of [
-4
View File
@@ -7,13 +7,9 @@ export function readShellElements(document = globalThis.document) {
_sliderContainer: document.getElementById('param-sliders'), _sliderContainer: document.getElementById('param-sliders'),
_ppToggles: document.getElementById('pp-toggles'), _ppToggles: document.getElementById('pp-toggles'),
_bloomBtn: document.getElementById('bloom-toggle'), _bloomBtn: document.getElementById('bloom-toggle'),
_bloomSliderRow: document.getElementById('bloom-slider-row'),
_bloomSlider: document.getElementById('bloom-intensity-slider'), _bloomSlider: document.getElementById('bloom-intensity-slider'),
_bloomSliderValue: document.getElementById('bloom-intensity-value'),
_sharpenBtn: document.getElementById('sharpen-toggle'), _sharpenBtn: document.getElementById('sharpen-toggle'),
_sharpenSliderRow: document.getElementById('sharpen-slider-row'),
_sharpenSlider: document.getElementById('sharpen-intensity-slider'), _sharpenSlider: document.getElementById('sharpen-intensity-slider'),
_sharpenSliderValue: document.getElementById('sharpen-intensity-value'),
_hudBtn: document.getElementById('hud-toggle'), _hudBtn: document.getElementById('hud-toggle'),
_hudLayoutRow: document.getElementById('hud-layout-row'), _hudLayoutRow: document.getElementById('hud-layout-row'),
_hudLayoutSelect: document.getElementById('hud-layout-select'), _hudLayoutSelect: document.getElementById('hud-layout-select'),
+5 -5
View File
@@ -697,13 +697,13 @@ export class ShellFacade {
} }
/** /**
* Maps a normalized sharpen value (0-1) to the unsharp-mask `amount` uniform. * Sets the sharpen amount the Display slider shows and applies it.
* Range: 0.1 (subtle) to 2.1 (aggressive edge enhancement). * @param {number} pct - Sharpen intensity, 0 to 100.
* @param {number} val - Normalized sharpen intensity (0.0 to 1.0). * @param {{syncShare?: boolean}} [options]
* @returns {void} * @returns {void}
*/ */
_applySharpenIntensity(val) { _setSharpenIntensity(pct, { syncShare = true } = {}) {
return this._visualSettings._applySharpenIntensity(...arguments); return this._visualSettings._setSharpenIntensity(...arguments);
} }
/** /**
+19
View File
@@ -0,0 +1,19 @@
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.
*/
export const useDisplayStore = defineStore('gev-display', () => {
/** The presented panel; null until VisualSettings first renders. */
const view = shallowRef(null);
/** @param {object} next A fresh presentation; every publish re-renders. */
function publish(next) {
view.value = markRaw(next);
}
return { view, publish };
});
+5 -185
View File
@@ -1,188 +1,8 @@
<script setup>
import DisplayPanel from './panels/DisplayPanel.vue';
</script>
<template> <template>
<!-- Post-processing toggles (top-right) --> <DisplayPanel />
<div id="pp-toggles" class="panel-collapsible collapsed" data-panel-id="pp-toggles">
<div class="compact pp-header-row">
<span data-i18n class="pp-header-label">DISPLAY</span>
<span class="panel-divider" aria-hidden="true"></span>
<button class="panel-collapse-btn pp-collapse-btn" data-collapse-target="pp-toggles" title="Collapse panel" data-i18n-title>&#x25C0;</button>
</div>
<div class="pp-panel-body" data-rail-scroller>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="hud-toggle" title="Intelligence HUD (H)" data-i18n-title>
<span class="pp-icon">&#x25CE;</span>
<span class="pp-label" translate="no">HUD</span>
</button>
<div class="pp-slider-row" id="hud-layout-row">
<span class="pp-slider-mini-label" data-i18n>Layout</span>
<select id="hud-layout-select" class="pp-select gev-field gev-field--compact" aria-label="HUD layout" data-i18n-aria-label>
<option value="tactical" data-i18n>Tactical</option>
<option value="minimal" data-i18n>Minimal</option>
<option value="cyber" data-i18n>Cyber</option>
</select>
</div>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-toggle-row" id="cyber-sonar-row">
<span class="pp-slider-mini-label" data-i18n>Sonar</span>
<button class="pp-mode-btn active" id="cyber-sonar-toggle" title="Cyber sonar contact scanner" aria-label="Cyber sonar contact scanner" aria-pressed="true" data-i18n-title data-i18n-aria-label data-i18n>ON</button>
</div>
<output id="cyber-sonar-status" class="cyber-sonar-control" role="status"></output>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-rings-row">
<label class="pp-slider-mini-label" for="cyber-sonar-rings" data-i18n>Rings</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-rings" min="3" max="12" step="1" value="8" aria-label="Sonar ring count" data-i18n-aria-label />
<span class="pp-slider-value" id="cyber-sonar-rings-value">8</span>
</div>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-range-row">
<label class="pp-slider-mini-label" for="cyber-sonar-range" data-i18n>Range</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-range" min="60" max="120" step="1" value="100" aria-label="Sonar ring range" data-i18n-aria-label />
<span class="pp-slider-value" id="cyber-sonar-range-value">100%</span>
</div>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-intensity-row">
<label class="pp-slider-mini-label" for="cyber-sonar-intensity" data-i18n>Power</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-intensity" min="0" max="100" step="1" value="70" aria-label="Sonar intensity" data-i18n-aria-label />
<span class="pp-slider-value" id="cyber-sonar-intensity-value">70%</span>
</div>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-opacity-row">
<label class="pp-slider-mini-label" for="cyber-sonar-opacity" data-i18n>Opacity</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-opacity" min="35" max="100" step="1" value="84" aria-label="Cyber contact and label opacity" title="Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="cyber-sonar-opacity-value">84%</span>
</div>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-sector-row">
<label class="pp-slider-mini-label" for="cyber-sonar-sector" data-i18n>Sector</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-sector" min="8" max="60" step="1" value="24" aria-label="Sonar sector width" data-i18n-aria-label />
<span class="pp-slider-value" id="cyber-sonar-sector-value">24°</span>
</div>
</div>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="detection-toggle" title="Detection Overlay (D)" aria-label="Detection overlay" aria-pressed="false" data-i18n-title data-i18n-aria-label>
<span class="pp-icon">&#x25A3;</span>
<span class="pp-label" data-i18n>DETECT</span>
</button>
<div class="pp-slider-row detection-slider-row" id="detection-slider-row">
<label class="pp-slider-mini-label" for="detection-density-slider" data-i18n>Density</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="detection-density-slider" min="0" max="100" step="25" value="50" aria-label="Detection label density" data-i18n-aria-label />
<span class="pp-slider-value" id="detection-density-value">50%</span>
</div>
<div class="pp-slider-row detection-allocation-row" id="detection-allocation-row">
<span class="pp-slider-mini-label" data-i18n>Allocation</span>
<div class="gev-segmented pp-mode-seg detection-allocation-seg" role="radiogroup" aria-label="Detection label allocation" data-i18n-aria-label>
<button class="gev-segment pp-mode-btn active" id="detection-allocation-elastic" data-allocation="ELASTIC" role="radio" aria-checked="true" data-i18n>Elastic</button>
<button class="gev-segment pp-mode-btn" id="detection-allocation-weighted" data-allocation="WEIGHTED" role="radio" aria-checked="false" data-i18n>Weighted</button>
</div>
</div>
<div class="pp-slider-row detection-fade-row" id="detection-fade-row">
<label class="pp-slider-mini-label" for="detection-fade-slider" data-i18n>Fade</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="detection-fade-slider" min="0" max="40" step="1" value="7" aria-label="Detection fade distance" title="World-overlay fade distance outside the keyhole as a percentage of its radius" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="detection-fade-value">7%</span>
</div>
<div class="pp-slider-row detection-opacity-row" id="detection-opacity-row">
<label class="pp-slider-mini-label" for="detection-opacity-slider" data-i18n>Outside</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="detection-opacity-slider" min="0" max="100" step="1" value="1" aria-label="Detection opacity outside the keyhole" title="World-overlay label and card opacity beyond the fade distance" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="detection-opacity-value">1%</span>
</div>
</div>
<!-- The one shared preset-parameter surface is inserted here at runtime. -->
<div id="param-slider-panel">
<div class="param-panel-header">
<span class="param-panel-title" data-i18n>PARAMETERS</span>
<span class="param-panel-divider"></span>
<button class="panel-collapse-btn" id="param-panel-collapse-btn" data-collapse-target="param-slider-panel" title="Collapse panel" data-i18n-title>−</button>
</div>
<div id="param-sliders"></div>
</div>
<div class="pp-toggle-group">
<!-- 3D aircraft is DEFAULT-ON in Proximity (owner directive 2026-08-22), so the
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>
<span class="pp-icon">&#x2708;</span>
<span class="pp-label">3D</span>
</button>
<div class="pp-slider-row visible" 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>
</div>
</div>
</div>
<div class="pp-toggle-group">
<label class="pp-toggle-btn pp-scope-toggle" title="Scope — the circular viewport mask" data-i18n-title>
<span class="pp-icon material-symbols-outlined" aria-hidden="true">adjust</span>
<span class="pp-label" data-i18n>Scope</span>
<input type="checkbox" id="scope-toggle" checked />
</label>
<div class="pp-slider-row visible" id="scope-slider-row">
<span class="pp-slider-mini-label" data-i18n>Feather</span>
<input type="range" class="gev-quantitative-slider pp-slider" id="scope-feather-slider" min="0" max="200" value="100" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="scope-feather-value">100%</span>
</div>
</div>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="draw-toggle" title="Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel" aria-pressed="false" data-i18n-title>
<span class="pp-icon material-symbols-outlined" aria-hidden="true">draw</span>
<span class="pp-label" data-i18n>Draw</span>
</button>
<div class="pp-slider-row" id="draw-mode-row">
<span class="pp-slider-mini-label" data-i18n>Shape</span>
<div class="gev-segmented pp-mode-seg" role="radiogroup" aria-label="Shape to draw" data-i18n-aria-label>
<button class="gev-segment pp-mode-btn active" data-shape="area" role="radio" aria-checked="true" data-i18n>Area</button>
<button class="gev-segment pp-mode-btn" data-shape="line" role="radio" aria-checked="false" data-i18n>Line</button>
<button class="gev-segment pp-mode-btn" data-shape="pin" role="radio" aria-checked="false" data-i18n>Pin</button>
</div>
</div>
<div class="pp-slider-row" id="draw-label-row">
<input type="text" class="pp-text-input" id="draw-label-input" maxlength="120" placeholder="Label (optional)" aria-label="Label for the drawn shape" data-i18n-placeholder data-i18n-aria-label autocomplete="off" spellcheck="false">
<select class="pp-select draw-color-select gev-field gev-field--compact" id="draw-color-select" aria-label="Colour of the drawn shape" data-i18n-aria-label>
<option value="primary" data-i18n>Primary</option>
<option value="amber" data-i18n>Amber</option>
<option value="cyan" data-i18n>Cyan</option>
<option value="green" data-i18n>Green</option>
<option value="red" data-i18n>Red</option>
</select>
<button class="pp-mode-btn draw-clear-btn" id="draw-clear" type="button" title="Remove every mark from the board" data-i18n-title data-i18n>Clear</button>
<span class="draw-hint" id="draw-hint" aria-live="polite"></span>
</div>
</div>
<div class="pp-toggle-group" id="geo-import-group">
<button class="pp-toggle-btn" id="geo-import-button" type="button" title="Show your own GeoJSON, KML, KMZ or GPX files on the globe (or drop them onto it). File bytes stay on this device; overlays can appear in map screenshots shared with voice." data-i18n-title>
<span class="pp-icon material-symbols-outlined" aria-hidden="true">upload_file</span>
<span class="pp-label" data-i18n>Import</span>
</button>
<input type="file" id="geo-import-input" accept=".geojson,.json,.kml,.kmz,.gpx" multiple hidden aria-label="Choose GeoJSON, KML, KMZ or GPX files" data-i18n-aria-label>
<ul class="geo-import-list" id="geo-import-list" aria-label="Imported files" data-i18n-aria-label></ul>
<span class="draw-hint geo-import-hint" id="geo-import-hint" aria-live="polite"></span>
</div>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="celestial-toggle" title="Celestial ring — reveal the full globe" aria-pressed="false" data-i18n-title>
<span class="pp-icon material-symbols-outlined" aria-hidden="true">flare</span>
<span class="pp-label" data-i18n>Celestial</span>
</button>
</div>
<button class="pp-toggle-btn" id="clean-view-toggle" title="Hide UI chrome" data-i18n-title>
<span class="pp-icon">&#x25A1;</span>
<span class="pp-label" data-i18n>Clean UI</span>
</button>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="bloom-toggle" title="Bloom / Glow" data-i18n-title>
<span class="pp-icon">&#x2728;</span>
<span class="pp-label" data-i18n>Bloom</span>
</button>
<div class="pp-slider-row" id="bloom-slider-row">
<input type="range" class="gev-quantitative-slider pp-slider" id="bloom-intensity-slider" min="0" max="200" value="0" aria-label="Bloom intensity" data-i18n-aria-label />
<span class="pp-slider-value" id="bloom-intensity-value">0%</span>
</div>
</div>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="sharpen-toggle" title="Sharpening" data-i18n-title>
<span class="pp-icon">&#x1F50D;</span>
<span class="pp-label" data-i18n>Sharpen</span>
</button>
<div class="pp-slider-row" id="sharpen-slider-row">
<input type="range" class="gev-quantitative-slider pp-slider" id="sharpen-intensity-slider" min="0" max="100" value="49" aria-label="Sharpen intensity" data-i18n-aria-label />
<span class="pp-slider-value" id="sharpen-intensity-value">49%</span>
</div>
</div>
</div>
</div>
<button id="clean-view-exit" title="Return UI controls" data-i18n-title data-i18n>EXIT CLEAN VIEW</button> <button id="clean-view-exit" title="Return UI controls" data-i18n-title data-i18n>EXIT CLEAN VIEW</button>
</template> </template>
+198
View File
@@ -0,0 +1,198 @@
<script setup>
import { storeToRefs } from 'pinia';
import { vRangeValue } from '../../rail/rangeValue.js';
import { useDisplayStore } from '../../stores/display.js';
// The Display panel. VisualSettings presents 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());
</script>
<template>
<!-- Post-processing toggles (top-right) -->
<div id="pp-toggles" class="panel-collapsible collapsed" data-panel-id="pp-toggles">
<div class="compact pp-header-row">
<span data-i18n class="pp-header-label">DISPLAY</span>
<span class="panel-divider" aria-hidden="true"></span>
<button class="panel-collapse-btn pp-collapse-btn" data-collapse-target="pp-toggles" title="Collapse panel" data-i18n-title>&#x25C0;</button>
</div>
<div class="pp-panel-body" data-rail-scroller>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="hud-toggle" title="Intelligence HUD (H)" data-i18n-title>
<span class="pp-icon">&#x25CE;</span>
<span class="pp-label" translate="no">HUD</span>
</button>
<div class="pp-slider-row" id="hud-layout-row">
<span class="pp-slider-mini-label" data-i18n>Layout</span>
<select id="hud-layout-select" class="pp-select gev-field gev-field--compact" aria-label="HUD layout" data-i18n-aria-label>
<option value="tactical" data-i18n>Tactical</option>
<option value="minimal" data-i18n>Minimal</option>
<option value="cyber" data-i18n>Cyber</option>
</select>
</div>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-toggle-row" id="cyber-sonar-row">
<span class="pp-slider-mini-label" data-i18n>Sonar</span>
<button class="pp-mode-btn active" id="cyber-sonar-toggle" title="Cyber sonar contact scanner" aria-label="Cyber sonar contact scanner" aria-pressed="true" data-i18n-title data-i18n-aria-label data-i18n>ON</button>
</div>
<output id="cyber-sonar-status" class="cyber-sonar-control" role="status"></output>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-rings-row">
<label class="pp-slider-mini-label" for="cyber-sonar-rings" data-i18n>Rings</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-rings" min="3" max="12" step="1" value="8" aria-label="Sonar ring count" data-i18n-aria-label />
<span class="pp-slider-value" id="cyber-sonar-rings-value">8</span>
</div>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-range-row">
<label class="pp-slider-mini-label" for="cyber-sonar-range" data-i18n>Range</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-range" min="60" max="120" step="1" value="100" aria-label="Sonar ring range" data-i18n-aria-label />
<span class="pp-slider-value" id="cyber-sonar-range-value">100%</span>
</div>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-intensity-row">
<label class="pp-slider-mini-label" for="cyber-sonar-intensity" data-i18n>Power</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-intensity" min="0" max="100" step="1" value="70" aria-label="Sonar intensity" data-i18n-aria-label />
<span class="pp-slider-value" id="cyber-sonar-intensity-value">70%</span>
</div>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-opacity-row">
<label class="pp-slider-mini-label" for="cyber-sonar-opacity" data-i18n>Opacity</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-opacity" min="35" max="100" step="1" value="84" aria-label="Cyber contact and label opacity" title="Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="cyber-sonar-opacity-value">84%</span>
</div>
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-sector-row">
<label class="pp-slider-mini-label" for="cyber-sonar-sector" data-i18n>Sector</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="cyber-sonar-sector" min="8" max="60" step="1" value="24" aria-label="Sonar sector width" data-i18n-aria-label />
<span class="pp-slider-value" id="cyber-sonar-sector-value">24°</span>
</div>
</div>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="detection-toggle" title="Detection Overlay (D)" aria-label="Detection overlay" aria-pressed="false" data-i18n-title data-i18n-aria-label>
<span class="pp-icon">&#x25A3;</span>
<span class="pp-label" data-i18n>DETECT</span>
</button>
<div class="pp-slider-row detection-slider-row" id="detection-slider-row">
<label class="pp-slider-mini-label" for="detection-density-slider" data-i18n>Density</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="detection-density-slider" min="0" max="100" step="25" value="50" aria-label="Detection label density" data-i18n-aria-label />
<span class="pp-slider-value" id="detection-density-value">50%</span>
</div>
<div class="pp-slider-row detection-allocation-row" id="detection-allocation-row">
<span class="pp-slider-mini-label" data-i18n>Allocation</span>
<div class="gev-segmented pp-mode-seg detection-allocation-seg" role="radiogroup" aria-label="Detection label allocation" data-i18n-aria-label>
<button class="gev-segment pp-mode-btn active" id="detection-allocation-elastic" data-allocation="ELASTIC" role="radio" aria-checked="true" data-i18n>Elastic</button>
<button class="gev-segment pp-mode-btn" id="detection-allocation-weighted" data-allocation="WEIGHTED" role="radio" aria-checked="false" data-i18n>Weighted</button>
</div>
</div>
<div class="pp-slider-row detection-fade-row" id="detection-fade-row">
<label class="pp-slider-mini-label" for="detection-fade-slider" data-i18n>Fade</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="detection-fade-slider" min="0" max="40" step="1" value="7" aria-label="Detection fade distance" title="World-overlay fade distance outside the keyhole as a percentage of its radius" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="detection-fade-value">7%</span>
</div>
<div class="pp-slider-row detection-opacity-row" id="detection-opacity-row">
<label class="pp-slider-mini-label" for="detection-opacity-slider" data-i18n>Outside</label>
<input type="range" class="gev-quantitative-slider pp-slider" id="detection-opacity-slider" min="0" max="100" step="1" value="1" aria-label="Detection opacity outside the keyhole" title="World-overlay label and card opacity beyond the fade distance" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="detection-opacity-value">1%</span>
</div>
</div>
<!-- The one shared preset-parameter surface is inserted here at runtime. -->
<div id="param-slider-panel">
<div class="param-panel-header">
<span class="param-panel-title" data-i18n>PARAMETERS</span>
<span class="param-panel-divider"></span>
<button class="panel-collapse-btn" id="param-panel-collapse-btn" data-collapse-target="param-slider-panel" title="Collapse panel" data-i18n-title>−</button>
</div>
<div id="param-sliders"></div>
</div>
<div class="pp-toggle-group">
<!-- 3D aircraft is DEFAULT-ON in Proximity (owner directive 2026-08-22), so the
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>
<span class="pp-icon">&#x2708;</span>
<span class="pp-label">3D</span>
</button>
<div class="pp-slider-row visible" 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>
</div>
</div>
</div>
<div class="pp-toggle-group">
<label class="pp-toggle-btn pp-scope-toggle" title="Scope — the circular viewport mask" data-i18n-title>
<span class="pp-icon material-symbols-outlined" aria-hidden="true">adjust</span>
<span class="pp-label" data-i18n>Scope</span>
<input type="checkbox" id="scope-toggle" checked />
</label>
<div class="pp-slider-row visible" id="scope-slider-row">
<span class="pp-slider-mini-label" data-i18n>Feather</span>
<input type="range" class="gev-quantitative-slider pp-slider" id="scope-feather-slider" min="0" max="200" value="100" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
<span class="pp-slider-value" id="scope-feather-value">100%</span>
</div>
</div>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="draw-toggle" title="Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel" aria-pressed="false" data-i18n-title>
<span class="pp-icon material-symbols-outlined" aria-hidden="true">draw</span>
<span class="pp-label" data-i18n>Draw</span>
</button>
<div class="pp-slider-row" id="draw-mode-row">
<span class="pp-slider-mini-label" data-i18n>Shape</span>
<div class="gev-segmented pp-mode-seg" role="radiogroup" aria-label="Shape to draw" data-i18n-aria-label>
<button class="gev-segment pp-mode-btn active" data-shape="area" role="radio" aria-checked="true" data-i18n>Area</button>
<button class="gev-segment pp-mode-btn" data-shape="line" role="radio" aria-checked="false" data-i18n>Line</button>
<button class="gev-segment pp-mode-btn" data-shape="pin" role="radio" aria-checked="false" data-i18n>Pin</button>
</div>
</div>
<div class="pp-slider-row" id="draw-label-row">
<input type="text" class="pp-text-input" id="draw-label-input" maxlength="120" placeholder="Label (optional)" aria-label="Label for the drawn shape" data-i18n-placeholder data-i18n-aria-label autocomplete="off" spellcheck="false">
<select class="pp-select draw-color-select gev-field gev-field--compact" id="draw-color-select" aria-label="Colour of the drawn shape" data-i18n-aria-label>
<option value="primary" data-i18n>Primary</option>
<option value="amber" data-i18n>Amber</option>
<option value="cyan" data-i18n>Cyan</option>
<option value="green" data-i18n>Green</option>
<option value="red" data-i18n>Red</option>
</select>
<button class="pp-mode-btn draw-clear-btn" id="draw-clear" type="button" title="Remove every mark from the board" data-i18n-title data-i18n>Clear</button>
<span class="draw-hint" id="draw-hint" aria-live="polite"></span>
</div>
</div>
<div class="pp-toggle-group" id="geo-import-group">
<button class="pp-toggle-btn" id="geo-import-button" type="button" title="Show your own GeoJSON, KML, KMZ or GPX files on the globe (or drop them onto it). File bytes stay on this device; overlays can appear in map screenshots shared with voice." data-i18n-title>
<span class="pp-icon material-symbols-outlined" aria-hidden="true">upload_file</span>
<span class="pp-label" data-i18n>Import</span>
</button>
<input type="file" id="geo-import-input" accept=".geojson,.json,.kml,.kmz,.gpx" multiple hidden aria-label="Choose GeoJSON, KML, KMZ or GPX files" data-i18n-aria-label>
<ul class="geo-import-list" id="geo-import-list" aria-label="Imported files" data-i18n-aria-label></ul>
<span class="draw-hint geo-import-hint" id="geo-import-hint" aria-live="polite"></span>
</div>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" id="celestial-toggle" title="Celestial ring — reveal the full globe" aria-pressed="false" data-i18n-title>
<span class="pp-icon material-symbols-outlined" aria-hidden="true">flare</span>
<span class="pp-label" data-i18n>Celestial</span>
</button>
</div>
<button class="pp-toggle-btn" id="clean-view-toggle" title="Hide UI chrome" data-i18n-title>
<span class="pp-icon">&#x25A1;</span>
<span class="pp-label" data-i18n>Clean UI</span>
</button>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" :class="{ active: view?.bloom.active }" id="bloom-toggle" title="Bloom / Glow" data-i18n-title>
<span class="pp-icon">&#x2728;</span>
<span class="pp-label" data-i18n>Bloom</span>
</button>
<div class="pp-slider-row" :class="{ visible: view?.bloom.active }" id="bloom-slider-row">
<input type="range" class="gev-quantitative-slider pp-slider" id="bloom-intensity-slider" min="0" max="200" value="0" v-range-value="view?.bloom.intensity ?? 0" aria-label="Bloom intensity" data-i18n-aria-label />
<span class="pp-slider-value" id="bloom-intensity-value">{{ view?.bloom.text ?? '0%' }}</span>
</div>
</div>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" :class="{ active: view?.sharpen.active }" id="sharpen-toggle" title="Sharpening" data-i18n-title>
<span class="pp-icon">&#x1F50D;</span>
<span class="pp-label" data-i18n>Sharpen</span>
</button>
<div class="pp-slider-row" :class="{ visible: view?.sharpen.active }" id="sharpen-slider-row">
<input type="range" class="gev-quantitative-slider pp-slider" id="sharpen-intensity-slider" min="0" max="100" value="49" v-range-value="view?.sharpen.intensity ?? 49" aria-label="Sharpen intensity" data-i18n-aria-label />
<span class="pp-slider-value" id="sharpen-intensity-value">{{ view?.sharpen.text ?? '49%' }}</span>
</div>
</div>
</div>
</div>
</template>
+72 -82
View File
@@ -3,6 +3,8 @@ import { syncScopeControls } from './displayControls.js';
import { STYLE_STATUS_LABELS } from './visualPresets.js'; import { STYLE_STATUS_LABELS } from './visualPresets.js';
import { RETIRED_STYLES } from '../visualStylePolicy.js'; import { RETIRED_STYLES } from '../visualStylePolicy.js';
import { syncSegmentedControl } from './segmentedControl.js'; import { syncSegmentedControl } from './segmentedControl.js';
import { useDisplayStore } from './stores/display.js';
import { applicationPinia } from './vueRuntime.js';
import { t } from '../i18n/index.js'; import { t } from '../i18n/index.js';
import { UiLifetime } from './uiLifetime.js'; import { UiLifetime } from './uiLifetime.js';
import { import {
@@ -102,16 +104,12 @@ export class VisualSettings {
} }
this._updateSliderPanel(style, { reveal: false }); this._updateSliderPanel(style, { reveal: false });
} }
if (typeof bloomIntensity === 'number' && this._bloomSlider) { if (typeof bloomIntensity === 'number') {
const intensity = decodeBloomIntensity(bloomIntensity, bloomVersion); const intensity = decodeBloomIntensity(bloomIntensity, bloomVersion);
this._setBloomIntensity(intensity, { syncShare: false }); this._setBloomIntensity(intensity, { syncShare: false });
} }
if (typeof sharpenIntensity === 'number' && this._sharpenSlider) { if (typeof sharpenIntensity === 'number')
const pct = Math.max(0, Math.min(100, Math.round(sharpenIntensity))); this._setSharpenIntensity(sharpenIntensity, { syncShare: false });
this._sharpenSlider.value = String(pct);
this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
}
if (typeof bloom === 'boolean') this._setBloomEnabled(bloom); if (typeof bloom === 'boolean') this._setBloomEnabled(bloom);
if (typeof sharpen === 'boolean') this._setSharpenEnabled(sharpen); if (typeof sharpen === 'boolean') this._setSharpenEnabled(sharpen);
if (hudVariant) this._setHudVariant(hudVariant); if (hudVariant) this._setHudVariant(hudVariant);
@@ -188,7 +186,9 @@ export class VisualSettings {
readContextMode, readContextMode,
readContextChanging, readContextChanging,
readDisplayPortalActive, readDisplayPortalActive,
store = useDisplayStore(applicationPinia()),
}) { }) {
this.store = store;
Object.assign(this, elements, operations, { Object.assign(this, elements, operations, {
viewer, viewer,
services, services,
@@ -209,6 +209,9 @@ export class VisualSettings {
holdRender: services.holdContinuousRender, holdRender: services.holdContinuousRender,
releaseRender: services.releaseContinuousRender, releaseRender: services.releaseContinuousRender,
}); });
// The sharpen amount as a whole percentage. VisualEffects keeps the
// fraction it applies, and drops writes once it is stopped.
this._sharpenPct = 49;
this.activeStyle = 'normal'; this.activeStyle = 'normal';
this._preCyberStyle = null; this._preCyberStyle = null;
this._cyberStyleUserSelected = false; this._cyberStyleUserSelected = false;
@@ -272,6 +275,16 @@ export class VisualSettings {
return this._visualEffects.sharpenEnabled; return this._visualEffects.sharpenEnabled;
} }
/** Bloom intensity as the Display slider shows it, 0-200. */
get bloomIntensityPct() {
return clampBloomIntensity(this._getBloomIntensity());
}
/** Sharpen amount as the Display slider shows it, 0-100. */
get sharpenIntensityPct() {
return this._sharpenPct;
}
_initStages() { _initStages() {
this._visualEffects.initStyles(); this._visualEffects.initStyles();
} }
@@ -456,9 +469,25 @@ export class VisualSettings {
} }
_initBloomSharpen() { _initBloomSharpen() {
this._visualEffects.initPostProcess( this._visualEffects.initPostProcess(this._sharpenPct / 100);
this._sharpenSlider ? parseInt(this._sharpenSlider.value, 10) / 100 : 0.6, this._publishDisplay();
); }
/** Present the Display panel's post-processing groups. */
_presentDisplay() {
const bloom = this.bloomIntensityPct;
return {
bloom: { active: this.bloomEnabled, intensity: bloom, text: `${bloom}%` },
sharpen: {
active: this.sharpenEnabled,
intensity: this._sharpenPct,
text: `${this._sharpenPct}%`,
},
};
}
_publishDisplay() {
this.store.publish(this._presentDisplay());
} }
_getBloomIntensity() { _getBloomIntensity() {
@@ -472,11 +501,8 @@ export class VisualSettings {
_setBloomIntensity(intensity, { syncShare = true } = {}) { _setBloomIntensity(intensity, { syncShare = true } = {}) {
const { governorRequestRender } = this.services; const { governorRequestRender } = this.services;
governorRequestRender('bloom'); governorRequestRender('bloom');
const clamped = clampBloomIntensity(intensity); this._applyBloomIntensity(clampBloomIntensity(intensity));
if (this._bloomSlider) this._bloomSlider.value = String(clamped); this._publishDisplay();
if (this._bloomSliderValue)
this._bloomSliderValue.textContent = `${clamped}%`;
this._applyBloomIntensity(clamped);
if (syncShare) this._syncShareState(); if (syncShare) this._syncShareState();
} }
@@ -489,8 +515,7 @@ export class VisualSettings {
governorRequestRender('bloom'); governorRequestRender('bloom');
this._visualEffects.setBloomEnabled(enabled); this._visualEffects.setBloomEnabled(enabled);
this._syncBloomStageEnabled(); this._syncBloomStageEnabled();
this._bloomBtn.classList.toggle('active', this.bloomEnabled); this._publishDisplay();
this._bloomSliderRow.classList.toggle('visible', this.bloomEnabled);
if (this.bloomEnabled) { if (this.bloomEnabled) {
this._applyBloomIntensity(this._getBloomIntensity()); this._applyBloomIntensity(this._getBloomIntensity());
} }
@@ -502,19 +527,22 @@ export class VisualSettings {
this._visualEffects.applySharpenIntensity(val); this._visualEffects.applySharpenIntensity(val);
} }
/** Set the sharpen amount, a percentage clamped to 0-100, and apply it. */
_setSharpenIntensity(pct, { syncShare = true } = {}) {
if (!Number.isFinite(pct)) return;
this._sharpenPct = Math.max(0, Math.min(100, Math.round(pct)));
this._applySharpenIntensity(this._sharpenPct / 100);
this._publishDisplay();
if (syncShare) this._syncShareState();
}
_setSharpenEnabled(enabled) { _setSharpenEnabled(enabled) {
const { governorRequestRender } = this.services; const { governorRequestRender } = this.services;
governorRequestRender('sharpen'); governorRequestRender('sharpen');
this._visualEffects.setSharpenEnabled(enabled); this._visualEffects.setSharpenEnabled(enabled);
this._sharpenBtn.classList.toggle('active', this.sharpenEnabled); this._publishDisplay();
if (this._sharpenSliderRow) { if (this.sharpenEnabled)
this._sharpenSliderRow.classList.toggle('visible', this.sharpenEnabled); this._applySharpenIntensity(this._sharpenPct / 100);
}
if (this.sharpenEnabled && this._sharpenSlider) {
this._applySharpenIntensity(
parseInt(this._sharpenSlider.value, 10) / 100,
);
}
this._syncShareState(); this._syncShareState();
this._layoutRightPanels(); this._layoutRightPanels();
} }
@@ -745,7 +773,7 @@ export class VisualSettings {
} }
const bloomInput = preset.bloom || {}; const bloomInput = preset.bloom || {};
if (typeof bloomInput.intensity === 'number' && this._bloomSlider) { if (typeof bloomInput.intensity === 'number') {
this._setBloomIntensity(clampBloomIntensity(bloomInput.intensity), { this._setBloomIntensity(clampBloomIntensity(bloomInput.intensity), {
syncShare: false, syncShare: false,
}); });
@@ -755,15 +783,8 @@ export class VisualSettings {
} }
const sharpenInput = preset.sharpen || {}; const sharpenInput = preset.sharpen || {};
if (typeof sharpenInput.intensity === 'number' && this._sharpenSlider) { if (typeof sharpenInput.intensity === 'number')
const sharpenPct = Math.max( this._setSharpenIntensity(sharpenInput.intensity, { syncShare: false });
0,
Math.min(100, Math.round(sharpenInput.intensity)),
);
this._sharpenSlider.value = String(sharpenPct);
this._sharpenSliderValue.textContent = `${sharpenPct}%`;
this._applySharpenIntensity(sharpenPct / 100);
}
if (typeof sharpenInput.enabled === 'boolean') { if (typeof sharpenInput.enabled === 'boolean') {
this._setSharpenEnabled(sharpenInput.enabled); this._setSharpenEnabled(sharpenInput.enabled);
} }
@@ -801,7 +822,7 @@ export class VisualSettings {
_applyGlobalPostDefaults() { _applyGlobalPostDefaults() {
const defaults = GLOBAL_POST_DEFAULTS; const defaults = GLOBAL_POST_DEFAULTS;
if (typeof defaults.bloom?.intensity === 'number' && this._bloomSlider) { if (typeof defaults.bloom?.intensity === 'number') {
this._setBloomIntensity(clampBloomIntensity(defaults.bloom.intensity), { this._setBloomIntensity(clampBloomIntensity(defaults.bloom.intensity), {
syncShare: false, syncShare: false,
}); });
@@ -810,18 +831,10 @@ export class VisualSettings {
this._setBloomEnabled(defaults.bloom.enabled); this._setBloomEnabled(defaults.bloom.enabled);
} }
if ( if (typeof defaults.sharpen?.intensity === 'number')
typeof defaults.sharpen?.intensity === 'number' && this._setSharpenIntensity(defaults.sharpen.intensity, {
this._sharpenSlider syncShare: false,
) { });
const sharpenPct = Math.max(
0,
Math.min(100, Math.round(defaults.sharpen.intensity)),
);
this._sharpenSlider.value = String(sharpenPct);
this._sharpenSliderValue.textContent = `${sharpenPct}%`;
this._applySharpenIntensity(sharpenPct / 100);
}
if (typeof defaults.sharpen?.enabled === 'boolean') { if (typeof defaults.sharpen?.enabled === 'boolean') {
this._setSharpenEnabled(defaults.sharpen.enabled); this._setSharpenEnabled(defaults.sharpen.enabled);
} }
@@ -892,7 +905,7 @@ export class VisualSettings {
options: { options: {
bloomIntensity: this._getBloomIntensity(), bloomIntensity: this._getBloomIntensity(),
bloomVersion: BLOOM_SCALE_VERSION, bloomVersion: BLOOM_SCALE_VERSION,
sharpenIntensity: parseInt(this._sharpenSlider?.value || '49', 10), sharpenIntensity: this._sharpenPct,
hudVariant: this.hud.getVariant(), hudVariant: this.hud.getVariant(),
hudVisible: this.hud.visible, hudVisible: this.hud.visible,
detectionMode: detection.mode, detectionMode: detection.mode,
@@ -1078,9 +1091,7 @@ export class VisualSettings {
setBloom({ enabled, intensityPct } = {}) { setBloom({ enabled, intensityPct } = {}) {
const current = () => ({ const current = () => ({
enabled: !!this.bloomEnabled, enabled: !!this.bloomEnabled,
intensityPct: this._bloomSlider intensityPct: this.bloomIntensityPct,
? parseInt(this._bloomSlider.value, 10)
: null,
}); });
if (enabled !== undefined && typeof enabled !== 'boolean') { if (enabled !== undefined && typeof enabled !== 'boolean') {
return { return {
@@ -1118,9 +1129,7 @@ export class VisualSettings {
setSharpen({ enabled, intensityPct } = {}) { setSharpen({ enabled, intensityPct } = {}) {
const current = () => ({ const current = () => ({
enabled: !!this.sharpenEnabled, enabled: !!this.sharpenEnabled,
intensityPct: this._sharpenSlider intensityPct: this.sharpenIntensityPct,
? parseInt(this._sharpenSlider.value, 10)
: null,
}); });
if (enabled !== undefined && typeof enabled !== 'boolean') { if (enabled !== undefined && typeof enabled !== 'boolean') {
return { return {
@@ -1144,12 +1153,7 @@ export class VisualSettings {
if (hasExplicitVisualChange) if (hasExplicitVisualChange)
this.shareLinkManager?.claimRestoreLane?.('visual'); this.shareLinkManager?.claimRestoreLane?.('visual');
if (intensityPct !== undefined) { if (intensityPct !== undefined) {
const pct = Math.round(Math.max(0, Math.min(100, intensityPct))); this._setSharpenIntensity(intensityPct);
if (this._sharpenSlider) this._sharpenSlider.value = String(pct);
if (this._sharpenSliderValue)
this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
this._syncShareState();
} }
if (enabled !== undefined) this._setSharpenEnabled(enabled); if (enabled !== undefined) this._setSharpenEnabled(enabled);
return { return {
@@ -1243,7 +1247,7 @@ export class VisualSettings {
}, },
sharpen: { sharpen: {
enabled: this.sharpenEnabled, enabled: this.sharpenEnabled,
intensity: parseInt(this._sharpenSlider?.value || '49', 10), intensity: this._sharpenPct,
}, },
hud: { hud: {
visible: this.hud.visible, visible: this.hud.visible,
@@ -1279,7 +1283,7 @@ export class VisualSettings {
} }
const bloomState = state.bloom || {}; const bloomState = state.bloom || {};
if (typeof bloomState.intensity === 'number' && this._bloomSlider) { if (typeof bloomState.intensity === 'number') {
const intensity = decodeBloomIntensity( const intensity = decodeBloomIntensity(
bloomState.intensity, bloomState.intensity,
bloomState.version ?? state.bloomVersion ?? BLOOM_SCALE_VERSION, bloomState.version ?? state.bloomVersion ?? BLOOM_SCALE_VERSION,
@@ -1291,15 +1295,8 @@ export class VisualSettings {
} }
const sharpenState = state.sharpen || {}; const sharpenState = state.sharpen || {};
if (typeof sharpenState.intensity === 'number' && this._sharpenSlider) { if (typeof sharpenState.intensity === 'number')
const pct = Math.max( this._setSharpenIntensity(sharpenState.intensity, { syncShare: false });
0,
Math.min(100, Math.round(sharpenState.intensity)),
);
this._sharpenSlider.value = String(pct);
this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
}
if (typeof sharpenState.enabled === 'boolean') { if (typeof sharpenState.enabled === 'boolean') {
this._setSharpenEnabled(sharpenState.enabled); this._setSharpenEnabled(sharpenState.enabled);
} }
@@ -1440,15 +1437,8 @@ export class VisualSettings {
typeof preset.sharpen === 'object' typeof preset.sharpen === 'object'
? preset.sharpen ? preset.sharpen
: { enabled: preset.sharpen }; : { enabled: preset.sharpen };
if (typeof sharpenInput.intensity === 'number' && this._sharpenSlider) { if (typeof sharpenInput.intensity === 'number')
const sharpenPct = Math.max( this._setSharpenIntensity(sharpenInput.intensity, { syncShare: false });
0,
Math.min(100, Math.round(sharpenInput.intensity)),
);
this._sharpenSlider.value = String(sharpenPct);
this._sharpenSliderValue.textContent = `${sharpenPct}%`;
this._applySharpenIntensity(sharpenPct / 100);
}
if (typeof sharpenInput.enabled === 'boolean') { if (typeof sharpenInput.enabled === 'boolean') {
this._setSharpenEnabled(sharpenInput.enabled); this._setSharpenEnabled(sharpenInput.enabled);
} else if (typeof sharpenInput.intensity === 'number') { } else if (typeof sharpenInput.intensity === 'number') {