From 343bdebe981138bc835b553917094ab293ce7640 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 30 Oct 2026 22:41:07 -0700 Subject: [PATCH] feat(ui): render the Display panel's bloom and sharpen from a store --- docs/VUE-MIGRATION.md | 5 +- scripts/package-boundaries.json | 15 +- scripts/qa-perf.mjs | 2 +- src/annotations/drawTool.test.mjs | 2 +- src/sharelink.celestial.test.mjs | 4 +- src/tooling/transitQa.test.mjs | 2 +- src/ui/applicationShell.js | 20 +-- src/ui/cyberSonarControls.test.mjs | 2 +- src/ui/cyberTheme.test.mjs | 7 +- src/ui/displayBindings.js | 5 +- src/ui/displayPanel.test.mjs | 123 +++++++++++++++++ src/ui/geoFileImport.test.mjs | 2 +- src/ui/shellElements.js | 4 - src/ui/shellFacade.js | 10 +- src/ui/stores/display.js | 19 +++ src/ui/views/DisplayControlsView.vue | 190 +------------------------ src/ui/views/panels/DisplayPanel.vue | 198 +++++++++++++++++++++++++++ src/ui/visualSettings.js | 154 ++++++++++----------- 18 files changed, 453 insertions(+), 311 deletions(-) create mode 100644 src/ui/displayPanel.test.mjs create mode 100644 src/ui/stores/display.js create mode 100644 src/ui/views/panels/DisplayPanel.vue diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 2c9478e11..b946286c8 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -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. diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index dbf39cef1..b967e8b4a 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -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"], diff --git a/scripts/qa-perf.mjs b/scripts/qa-perf.mjs index 6aa5f9e89..33302a884 100644 --- a/scripts/qa-perf.mjs +++ b/scripts/qa-perf.mjs @@ -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; } diff --git a/src/annotations/drawTool.test.mjs b/src/annotations/drawTool.test.mjs index 3db0d8724..094b07b06 100644 --- a/src/annotations/drawTool.test.mjs +++ b/src/annotations/drawTool.test.mjs @@ -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', diff --git a/src/sharelink.celestial.test.mjs b/src/sharelink.celestial.test.mjs index 91961e0b1..054e6d035 100644 --- a/src/sharelink.celestial.test.mjs +++ b/src/sharelink.celestial.test.mjs @@ -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) { diff --git a/src/tooling/transitQa.test.mjs b/src/tooling/transitQa.test.mjs index b696d120a..adbf9875d 100644 --- a/src/tooling/transitQa.test.mjs +++ b/src/tooling/transitQa.test.mjs @@ -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"/); diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index 07e89705a..5f9b11f8a 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -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, diff --git a/src/ui/cyberSonarControls.test.mjs b/src/ui/cyberSonarControls.test.mjs index 63fd31361..5ab764e80 100644 --- a/src/ui/cyberSonarControls.test.mjs +++ b/src/ui/cyberSonarControls.test.mjs @@ -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"`)); } diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs index 05bb9ced1..6b0d036e8 100644 --- a/src/ui/cyberTheme.test.mjs +++ b/src/ui/cyberTheme.test.mjs @@ -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"/); diff --git a/src/ui/displayBindings.js b/src/ui/displayBindings.js index 3ae874c3c..251c7040e 100644 --- a/src/ui/displayBindings.js +++ b/src/ui/displayBindings.js @@ -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'); diff --git a/src/ui/displayPanel.test.mjs b/src/ui/displayPanel.test.mjs new file mode 100644 index 000000000..531ec97a6 --- /dev/null +++ b/src/ui/displayPanel.test.mjs @@ -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'); +}); diff --git a/src/ui/geoFileImport.test.mjs b/src/ui/geoFileImport.test.mjs index f88331b4d..a01193dde 100644 --- a/src/ui/geoFileImport.test.mjs +++ b/src/ui/geoFileImport.test.mjs @@ -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 [ diff --git a/src/ui/shellElements.js b/src/ui/shellElements.js index e6523a0fc..fd7c3e27c 100644 --- a/src/ui/shellElements.js +++ b/src/ui/shellElements.js @@ -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'), diff --git a/src/ui/shellFacade.js b/src/ui/shellFacade.js index 898d3af69..5cc7772bf 100644 --- a/src/ui/shellFacade.js +++ b/src/ui/shellFacade.js @@ -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); } /** diff --git a/src/ui/stores/display.js b/src/ui/stores/display.js new file mode 100644 index 000000000..37ec45a89 --- /dev/null +++ b/src/ui/stores/display.js @@ -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 }; +}); diff --git a/src/ui/views/DisplayControlsView.vue b/src/ui/views/DisplayControlsView.vue index 1dddc6cc3..b04e4a344 100644 --- a/src/ui/views/DisplayControlsView.vue +++ b/src/ui/views/DisplayControlsView.vue @@ -1,188 +1,8 @@ + + diff --git a/src/ui/views/panels/DisplayPanel.vue b/src/ui/views/panels/DisplayPanel.vue new file mode 100644 index 000000000..8ef9c7198 --- /dev/null +++ b/src/ui/views/panels/DisplayPanel.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/ui/visualSettings.js b/src/ui/visualSettings.js index 7c5a881eb..b5c51257f 100644 --- a/src/ui/visualSettings.js +++ b/src/ui/visualSettings.js @@ -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') {