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
renders `LayerRows` synchronously; keyed chips and list items keep focus
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
directly gain a slot instead.
+11 -4
View File
@@ -51,7 +51,10 @@
"src/ui/cctv/CctvPicker.vue",
"src/ui/stores/cctv.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"]
},
@@ -955,7 +958,8 @@
"src/ui/rail/rangeValue.js",
"src/ui/weather/WeatherReadout.vue",
"src/ui/stores/streetLevel.js",
"src/ui/stores/cctv.js"
"src/ui/stores/cctv.js",
"src/ui/stores/display.js"
],
"external": ["cesium", "vue", "pinia"]
},
@@ -2922,7 +2926,8 @@
"src/ui/recentImagery/RecentImageryCard.vue",
"src/ui/recentImagery/RecentImageryReadout.vue",
"src/ui/stores/streetLevel.js",
"src/ui/stores/cctv.js"
"src/ui/stores/cctv.js",
"src/ui/stores/display.js"
],
"external": [
"hls.js",
@@ -3670,6 +3675,8 @@
"src/ui/visualInput.js",
"src/ui/visualPresets.js",
"src/ui/visualSettings.js",
"src/ui/stores/display.js",
"src/ui/vueRuntime.js",
"src/ui/segmentedControl.js",
"src/i18n/marker.js",
"src/i18n/index.js",
@@ -3688,7 +3695,7 @@
"src/ui/displayControls.js",
"src/visualStylePolicy.js"
],
"external": ["cesium"]
"external": ["cesium", "vue", "pinia"]
},
"ui-panel-chrome": {
"exports": ["./ui/panel-chrome"],
+1 -1
View File
@@ -310,7 +310,7 @@ try {
countFrames(2_500),
page.evaluate(() => new Promise((resolve) => setTimeout(() => {
// Drive the actual sharpen slider: input event → handler →
// _applySharpenIntensity → governorRequestRender. Proves the wiring,
// _setSharpenIntensity → governorRequestRender. Proves the wiring,
// not just the facade.
const slider = document.getElementById('sharpen-intensity-slider');
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', () => {
// The DISPLAY rail is a Vue view now, rendered into index.html's view host —
// 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 = [
'draw-toggle',
'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'],
['setScopeEnabled:', 'setScopeFeather:', 'setScopeMaskEnabled(', 'setScopeEnabled control'],
['setScopeFeather:', 'setSharpenIntensity:', 'setScopeMaskFeather(', 'setScopeFeather control'],
['setSharpenIntensity:', 'setHudLayout:', 'this._applySharpenIntensity(', 'setSharpenIntensity control'],
['setSharpenIntensity:', 'setHudLayout:', 'this._setSharpenIntensity(', 'setSharpenIntensity control'],
['setHudLayout:', 'toggleCleanView:', 'this._setHudVariant(', 'setHudLayout control'],
['setDensity:', 'setAllocation:', 'this._applyDetectionDensityFromUi()', 'setDensity 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'],
[' setDetection({ enabled, mode, densityPct, allocationStrategy, fadePct, outsideOpacityPct } = {}) {', ' async setMapStack(stackId) {', 'this._setDetectionAllocation(', 'setDetection'],
[' 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'],
];
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.deepEqual(modes, ['BALANCED', 'OFF', 'DENSE']);
const template = readFileSync(
new URL('../ui/views/DisplayControlsView.vue', import.meta.url),
new URL('../ui/views/panels/DisplayPanel.vue', import.meta.url),
'utf8',
);
assert.match(template, /id="detection-density-slider"/);
+3 -17
View File
@@ -207,10 +207,6 @@ export class StyleManager extends ShellFacade {
setScopeMaskFeather: services.setScopeMaskFeather,
},
elements: {
_bloomBtn: this._bloomBtn,
_bloomSlider: this._bloomSlider,
_bloomSliderRow: this._bloomSliderRow,
_bloomSliderValue: this._bloomSliderValue,
_celestialBtn: this._celestialBtn,
_cockpitDisplayToggleBtn: this._cockpitDisplayToggleBtn,
_detectionAllocationRow: this._detectionAllocationRow,
@@ -242,10 +238,6 @@ export class StyleManager extends ShellFacade {
_scopeBtn: this._scopeBtn,
_scopeFeatherSlider: this._scopeFeatherSlider,
_scopeFeatherValue: this._scopeFeatherValue,
_sharpenBtn: this._sharpenBtn,
_sharpenSlider: this._sharpenSlider,
_sharpenSliderRow: this._sharpenSliderRow,
_sharpenSliderValue: this._sharpenSliderValue,
_sliderContainer: this._sliderContainer,
_sliderPanel: this._sliderPanel,
_styleIndicator: this._styleIndicator,
@@ -552,7 +544,6 @@ export class StyleManager extends ShellFacade {
_celestialBtn: this._celestialBtn,
_models3dBtn: this._models3dBtn,
_scopeFeatherValue: this._scopeFeatherValue,
_sharpenSliderValue: this._sharpenSliderValue,
_detectionDensityValue: this._detectionDensityValue,
},
operations: {
@@ -565,8 +556,7 @@ export class StyleManager extends ShellFacade {
_setBloomEnabled: (...args) => this._setBloomEnabled(...args),
_setBloomIntensity: (...args) => this._setBloomIntensity(...args),
_setSharpenEnabled: (...args) => this._setSharpenEnabled(...args),
_applySharpenIntensity: (...args) =>
this._applySharpenIntensity(...args),
_setSharpenIntensity: (...args) => this._setSharpenIntensity(...args),
_setHudVariant: (...args) => this._setHudVariant(...args),
_setCyberSonarEnabled: (...args) => this._setCyberSonarEnabled(...args),
_setCyberSonarSetting: (...args) => this._setCyberSonarSetting(...args),
@@ -1484,15 +1474,11 @@ export class StyleManager extends ShellFacade {
detection: this.getDetectionState(),
bloom: {
enabled: !!this.bloomEnabled,
intensityPct: this._bloomSlider
? parseInt(this._bloomSlider.value, 10)
: null,
intensityPct: this._visualSettings.bloomIntensityPct,
},
sharpen: {
enabled: !!this.sharpenEnabled,
intensityPct: this._sharpenSlider
? parseInt(this._sharpenSlider.value, 10)
: null,
intensityPct: this._visualSettings.sharpenIntensityPct,
},
celestialRing: {
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', () => {
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']) {
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',
'_scheduleAdaptivePanelLayout',
'_applyDetectionFadeFromUi',
// Intensities: the fixture has no post-process stages to drive.
'_setBloomIntensity',
'_setSharpenIntensity',
])
owner[name] = () => {};
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', () => {
const display = read('./views/DisplayControlsView.vue');
const display = read('./views/panels/DisplayPanel.vue');
const stylesheet = read('../../style.css');
const rightRail = read('./rightPanelRail.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', () => {
const template = readFileSync(
new URL('./views/DisplayControlsView.vue', import.meta.url),
new URL('./views/panels/DisplayPanel.vue', import.meta.url),
'utf8',
);
assert.match(template, /id="cyber-sonar-toggle"/);
+1 -4
View File
@@ -156,10 +156,7 @@ export class DisplayBindings {
},
setSharpenIntensity: (pct) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
if (this._sharpenSliderValue)
this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
this._syncShareState();
this._setSharpenIntensity(pct);
},
setHudLayout: (value) => {
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);
const markup = fs.readFileSync(
new URL('./views/DisplayControlsView.vue', import.meta.url),
new URL('./views/panels/DisplayPanel.vue', import.meta.url),
'utf8',
);
for (const id of [
-4
View File
@@ -7,13 +7,9 @@ export function readShellElements(document = globalThis.document) {
_sliderContainer: document.getElementById('param-sliders'),
_ppToggles: document.getElementById('pp-toggles'),
_bloomBtn: document.getElementById('bloom-toggle'),
_bloomSliderRow: document.getElementById('bloom-slider-row'),
_bloomSlider: document.getElementById('bloom-intensity-slider'),
_bloomSliderValue: document.getElementById('bloom-intensity-value'),
_sharpenBtn: document.getElementById('sharpen-toggle'),
_sharpenSliderRow: document.getElementById('sharpen-slider-row'),
_sharpenSlider: document.getElementById('sharpen-intensity-slider'),
_sharpenSliderValue: document.getElementById('sharpen-intensity-value'),
_hudBtn: document.getElementById('hud-toggle'),
_hudLayoutRow: document.getElementById('hud-layout-row'),
_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.
* Range: 0.1 (subtle) to 2.1 (aggressive edge enhancement).
* @param {number} val - Normalized sharpen intensity (0.0 to 1.0).
* Sets the sharpen amount the Display slider shows and applies it.
* @param {number} pct - Sharpen intensity, 0 to 100.
* @param {{syncShare?: boolean}} [options]
* @returns {void}
*/
_applySharpenIntensity(val) {
return this._visualSettings._applySharpenIntensity(...arguments);
_setSharpenIntensity(pct, { syncShare = true } = {}) {
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>
<!-- 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" 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>
<DisplayPanel />
<button id="clean-view-exit" title="Return UI controls" data-i18n-title data-i18n>EXIT CLEAN VIEW</button>
</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 { RETIRED_STYLES } from '../visualStylePolicy.js';
import { syncSegmentedControl } from './segmentedControl.js';
import { useDisplayStore } from './stores/display.js';
import { applicationPinia } from './vueRuntime.js';
import { t } from '../i18n/index.js';
import { UiLifetime } from './uiLifetime.js';
import {
@@ -102,16 +104,12 @@ export class VisualSettings {
}
this._updateSliderPanel(style, { reveal: false });
}
if (typeof bloomIntensity === 'number' && this._bloomSlider) {
if (typeof bloomIntensity === 'number') {
const intensity = decodeBloomIntensity(bloomIntensity, bloomVersion);
this._setBloomIntensity(intensity, { syncShare: false });
}
if (typeof sharpenIntensity === 'number' && this._sharpenSlider) {
const pct = Math.max(0, Math.min(100, Math.round(sharpenIntensity)));
this._sharpenSlider.value = String(pct);
this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
}
if (typeof sharpenIntensity === 'number')
this._setSharpenIntensity(sharpenIntensity, { syncShare: false });
if (typeof bloom === 'boolean') this._setBloomEnabled(bloom);
if (typeof sharpen === 'boolean') this._setSharpenEnabled(sharpen);
if (hudVariant) this._setHudVariant(hudVariant);
@@ -188,7 +186,9 @@ export class VisualSettings {
readContextMode,
readContextChanging,
readDisplayPortalActive,
store = useDisplayStore(applicationPinia()),
}) {
this.store = store;
Object.assign(this, elements, operations, {
viewer,
services,
@@ -209,6 +209,9 @@ export class VisualSettings {
holdRender: services.holdContinuousRender,
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._preCyberStyle = null;
this._cyberStyleUserSelected = false;
@@ -272,6 +275,16 @@ export class VisualSettings {
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() {
this._visualEffects.initStyles();
}
@@ -456,9 +469,25 @@ export class VisualSettings {
}
_initBloomSharpen() {
this._visualEffects.initPostProcess(
this._sharpenSlider ? parseInt(this._sharpenSlider.value, 10) / 100 : 0.6,
);
this._visualEffects.initPostProcess(this._sharpenPct / 100);
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() {
@@ -472,11 +501,8 @@ export class VisualSettings {
_setBloomIntensity(intensity, { syncShare = true } = {}) {
const { governorRequestRender } = this.services;
governorRequestRender('bloom');
const clamped = clampBloomIntensity(intensity);
if (this._bloomSlider) this._bloomSlider.value = String(clamped);
if (this._bloomSliderValue)
this._bloomSliderValue.textContent = `${clamped}%`;
this._applyBloomIntensity(clamped);
this._applyBloomIntensity(clampBloomIntensity(intensity));
this._publishDisplay();
if (syncShare) this._syncShareState();
}
@@ -489,8 +515,7 @@ export class VisualSettings {
governorRequestRender('bloom');
this._visualEffects.setBloomEnabled(enabled);
this._syncBloomStageEnabled();
this._bloomBtn.classList.toggle('active', this.bloomEnabled);
this._bloomSliderRow.classList.toggle('visible', this.bloomEnabled);
this._publishDisplay();
if (this.bloomEnabled) {
this._applyBloomIntensity(this._getBloomIntensity());
}
@@ -502,19 +527,22 @@ export class VisualSettings {
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) {
const { governorRequestRender } = this.services;
governorRequestRender('sharpen');
this._visualEffects.setSharpenEnabled(enabled);
this._sharpenBtn.classList.toggle('active', this.sharpenEnabled);
if (this._sharpenSliderRow) {
this._sharpenSliderRow.classList.toggle('visible', this.sharpenEnabled);
}
if (this.sharpenEnabled && this._sharpenSlider) {
this._applySharpenIntensity(
parseInt(this._sharpenSlider.value, 10) / 100,
);
}
this._publishDisplay();
if (this.sharpenEnabled)
this._applySharpenIntensity(this._sharpenPct / 100);
this._syncShareState();
this._layoutRightPanels();
}
@@ -745,7 +773,7 @@ export class VisualSettings {
}
const bloomInput = preset.bloom || {};
if (typeof bloomInput.intensity === 'number' && this._bloomSlider) {
if (typeof bloomInput.intensity === 'number') {
this._setBloomIntensity(clampBloomIntensity(bloomInput.intensity), {
syncShare: false,
});
@@ -755,15 +783,8 @@ export class VisualSettings {
}
const sharpenInput = preset.sharpen || {};
if (typeof sharpenInput.intensity === 'number' && this._sharpenSlider) {
const sharpenPct = Math.max(
0,
Math.min(100, Math.round(sharpenInput.intensity)),
);
this._sharpenSlider.value = String(sharpenPct);
this._sharpenSliderValue.textContent = `${sharpenPct}%`;
this._applySharpenIntensity(sharpenPct / 100);
}
if (typeof sharpenInput.intensity === 'number')
this._setSharpenIntensity(sharpenInput.intensity, { syncShare: false });
if (typeof sharpenInput.enabled === 'boolean') {
this._setSharpenEnabled(sharpenInput.enabled);
}
@@ -801,7 +822,7 @@ export class VisualSettings {
_applyGlobalPostDefaults() {
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), {
syncShare: false,
});
@@ -810,18 +831,10 @@ export class VisualSettings {
this._setBloomEnabled(defaults.bloom.enabled);
}
if (
typeof defaults.sharpen?.intensity === 'number' &&
this._sharpenSlider
) {
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?.intensity === 'number')
this._setSharpenIntensity(defaults.sharpen.intensity, {
syncShare: false,
});
if (typeof defaults.sharpen?.enabled === 'boolean') {
this._setSharpenEnabled(defaults.sharpen.enabled);
}
@@ -892,7 +905,7 @@ export class VisualSettings {
options: {
bloomIntensity: this._getBloomIntensity(),
bloomVersion: BLOOM_SCALE_VERSION,
sharpenIntensity: parseInt(this._sharpenSlider?.value || '49', 10),
sharpenIntensity: this._sharpenPct,
hudVariant: this.hud.getVariant(),
hudVisible: this.hud.visible,
detectionMode: detection.mode,
@@ -1078,9 +1091,7 @@ export class VisualSettings {
setBloom({ enabled, intensityPct } = {}) {
const current = () => ({
enabled: !!this.bloomEnabled,
intensityPct: this._bloomSlider
? parseInt(this._bloomSlider.value, 10)
: null,
intensityPct: this.bloomIntensityPct,
});
if (enabled !== undefined && typeof enabled !== 'boolean') {
return {
@@ -1118,9 +1129,7 @@ export class VisualSettings {
setSharpen({ enabled, intensityPct } = {}) {
const current = () => ({
enabled: !!this.sharpenEnabled,
intensityPct: this._sharpenSlider
? parseInt(this._sharpenSlider.value, 10)
: null,
intensityPct: this.sharpenIntensityPct,
});
if (enabled !== undefined && typeof enabled !== 'boolean') {
return {
@@ -1144,12 +1153,7 @@ export class VisualSettings {
if (hasExplicitVisualChange)
this.shareLinkManager?.claimRestoreLane?.('visual');
if (intensityPct !== undefined) {
const pct = Math.round(Math.max(0, Math.min(100, intensityPct)));
if (this._sharpenSlider) this._sharpenSlider.value = String(pct);
if (this._sharpenSliderValue)
this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
this._syncShareState();
this._setSharpenIntensity(intensityPct);
}
if (enabled !== undefined) this._setSharpenEnabled(enabled);
return {
@@ -1243,7 +1247,7 @@ export class VisualSettings {
},
sharpen: {
enabled: this.sharpenEnabled,
intensity: parseInt(this._sharpenSlider?.value || '49', 10),
intensity: this._sharpenPct,
},
hud: {
visible: this.hud.visible,
@@ -1279,7 +1283,7 @@ export class VisualSettings {
}
const bloomState = state.bloom || {};
if (typeof bloomState.intensity === 'number' && this._bloomSlider) {
if (typeof bloomState.intensity === 'number') {
const intensity = decodeBloomIntensity(
bloomState.intensity,
bloomState.version ?? state.bloomVersion ?? BLOOM_SCALE_VERSION,
@@ -1291,15 +1295,8 @@ export class VisualSettings {
}
const sharpenState = state.sharpen || {};
if (typeof sharpenState.intensity === 'number' && this._sharpenSlider) {
const pct = Math.max(
0,
Math.min(100, Math.round(sharpenState.intensity)),
);
this._sharpenSlider.value = String(pct);
this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
}
if (typeof sharpenState.intensity === 'number')
this._setSharpenIntensity(sharpenState.intensity, { syncShare: false });
if (typeof sharpenState.enabled === 'boolean') {
this._setSharpenEnabled(sharpenState.enabled);
}
@@ -1440,15 +1437,8 @@ export class VisualSettings {
typeof preset.sharpen === 'object'
? preset.sharpen
: { enabled: preset.sharpen };
if (typeof sharpenInput.intensity === 'number' && this._sharpenSlider) {
const sharpenPct = Math.max(
0,
Math.min(100, Math.round(sharpenInput.intensity)),
);
this._sharpenSlider.value = String(sharpenPct);
this._sharpenSliderValue.textContent = `${sharpenPct}%`;
this._applySharpenIntensity(sharpenPct / 100);
}
if (typeof sharpenInput.intensity === 'number')
this._setSharpenIntensity(sharpenInput.intensity, { syncShare: false });
if (typeof sharpenInput.enabled === 'boolean') {
this._setSharpenEnabled(sharpenInput.enabled);
} else if (typeof sharpenInput.intensity === 'number') {