mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
feat(ui): render the Display panel's bloom and sharpen from a store
This commit is contained in:
1 parent
5707a8a151
commit
343bdebe98
18 files changed
+453
-311
No files matched your search
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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; }
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"/);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"`));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"/);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
@@ -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 [
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
});
|
||||||
@@ -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>◀</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">◎</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">▣</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">✈</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">□</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">✨</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">🔍</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>
|
||||||
@@ -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>◀</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">◎</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">▣</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">✈</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">□</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">✨</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">🔍</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
@@ -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') {
|
||||||
|
|||||||
Reference in new issue
Block a user