mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
feat(display): render the style parameters from the display store
VisualSettings publishes the active style's parameter rows to the display store, and the Display panel renders them with the shared row component; a moved slider goes back through the store's setStyleParameter action. The readout snaps a value to its step as the browser does, half-steps up and an off-grid maximum down. A preset that resets the shown style's uniforms republishes its rows, input for another style's parameters is ignored, and a stopped owner takes its rows with it unless a newer owner has published since. createStyleParameters is deprecated and builds its rows the same way.
This commit is contained in:
1 parent
3a750c9021
commit
c3c69a91cd
14 files changed
+316
-80
No files matched your search
+5
-2
@@ -77,8 +77,11 @@
|
|||||||
which also carries the camera's tilt and compass heading. The recording
|
which also carries the camera's tilt and compass heading. The recording
|
||||||
safe frame renders what `RecordingControls` publishes to the new
|
safe frame renders what `RecordingControls` publishes to the new
|
||||||
recording store. `syncScopeControls` (`./ui/display`),
|
recording store. `syncScopeControls` (`./ui/display`),
|
||||||
`shouldHideCollapsedRightPanels` (`./ui/layout`) and
|
`shouldHideCollapsedRightPanels` (`./ui/layout`),
|
||||||
`bindClearLayersControl` (`./ui/layers`) are deprecated: the Display view,
|
`bindClearLayersControl` (`./ui/layers`) and `createStyleParameters`
|
||||||
|
(`./ui/input`; the Display panel renders the style parameters
|
||||||
|
VisualSettings publishes to the display store) are deprecated: the Display
|
||||||
|
view,
|
||||||
the rail and the globe actions view now own what they did. `./ui/input` imports Vue
|
the rail and the globe actions view now own what they did. `./ui/input` imports Vue
|
||||||
(samehkhamis).
|
(samehkhamis).
|
||||||
|
|
||||||
|
|||||||
@@ -170,8 +170,9 @@ a feature supplying only its accent.
|
|||||||
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 (in progress: `DisplayPanel` renders
|
descriptor before acting), Display (in progress: `DisplayPanel` renders
|
||||||
the HUD, Sonar, Scope, Celestial, detection, bloom and sharpen from the
|
the HUD, Sonar, Scope, Celestial, detection, bloom, sharpen and the
|
||||||
`gev-display` store, which VisualSettings publishes to, and the 3D
|
active style's parameter sliders from the `gev-display` store, which
|
||||||
|
VisualSettings publishes to and answers the sliders through, and the 3D
|
||||||
aircraft group, the Draw controls and the imported file list, which
|
aircraft group, the Draw controls and the imported file list, which
|
||||||
AircraftDisplay, the draw tool and the file import publish beside it;
|
AircraftDisplay, the draw tool and the file import publish beside it;
|
||||||
the controls call DisplayBindings, the draw tool and the file import
|
the controls call DisplayBindings, the draw tool and the file import
|
||||||
|
|||||||
@@ -142,7 +142,8 @@
|
|||||||
"src/ui/stores/globeActions.js",
|
"src/ui/stores/globeActions.js",
|
||||||
"src/ui/views/panels/GlobeActions.vue",
|
"src/ui/views/panels/GlobeActions.vue",
|
||||||
"src/ui/views/panels/GlobeCompass.vue",
|
"src/ui/views/panels/GlobeCompass.vue",
|
||||||
"src/ui/stores/recording.js"
|
"src/ui/stores/recording.js",
|
||||||
|
"src/ui/styleParameters.js"
|
||||||
],
|
],
|
||||||
"external": ["vue", "pinia"]
|
"external": ["vue", "pinia"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -224,13 +224,14 @@ test('parameterized Display presets keep one stable scroll owner', () => {
|
|||||||
assert.match(css, /#pp-toggles:not\(\.collapsed\) > \.pp-panel-body > #param-slider-panel\.active\s*\{[\s\S]*?flex:\s*0 0 auto;[\s\S]*?max-height:\s*none;[\s\S]*?overflow-y:\s*visible;/);
|
assert.match(css, /#pp-toggles:not\(\.collapsed\) > \.pp-panel-body > #param-slider-panel\.active\s*\{[\s\S]*?flex:\s*0 0 auto;[\s\S]*?max-height:\s*none;[\s\S]*?overflow-y:\s*visible;/);
|
||||||
assert.match(ui, /readDisplayScrollTop: \(\) =>\s*this\._displayPortalScrollRestoreOwner === 'standard'[\s\S]*?this\._standardDisplayScrollTop[\s\S]*?displayPanelScroller\(this\._ppToggles\)\?\.scrollTop \|\| 0/);
|
assert.match(ui, /readDisplayScrollTop: \(\) =>\s*this\._displayPortalScrollRestoreOwner === 'standard'[\s\S]*?this\._standardDisplayScrollTop[\s\S]*?displayPanelScroller\(this\._ppToggles\)\?\.scrollTop \|\| 0/);
|
||||||
assert.match(rightRail, /scroller\.scrollTop = Math\.min\(displayScrollTop, maxScrollTop\);/);
|
assert.match(rightRail, /scroller\.scrollTop = Math\.min\(displayScrollTop, maxScrollTop\);/);
|
||||||
|
// The panel's parameters change, and the rail lays out again.
|
||||||
assert.match(
|
assert.match(
|
||||||
ui,
|
ui,
|
||||||
/this\._sliderPanel\.classList\.remove\('active'\);\s*this\._scheduleRightPanelLayout\(\);/,
|
/this\._publishParameters\(null\);\s*this\._scheduleRightPanelLayout\(\);/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
ui,
|
ui,
|
||||||
/this\._sliderPanel\.classList\.add\('active'\);\s*this\._scheduleRightPanelLayout\(\);/,
|
/this\._publishParameters\(\{[\s\S]*?\}\);\s*this\._scheduleRightPanelLayout\(\);/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
css,
|
css,
|
||||||
|
|||||||
@@ -641,8 +641,8 @@ test('every explicit visual control facade claims restore authority before mutat
|
|||||||
|
|
||||||
const style = sourceBlock(' setStyle(styleName, {', ' _startTransition(styleName, fromValue, toValue) {');
|
const style = sourceBlock(' setStyle(styleName, {', ' _startTransition(styleName, fromValue, toValue) {');
|
||||||
assertClaimsBefore(style, 'this.activeStyle = styleName', 'setStyle');
|
assertClaimsBefore(style, 'this.activeStyle = styleName', 'setStyle');
|
||||||
const sliders = sourceBlock(' _updateSliderPanel(styleName, { reveal = false } = {}) {', ' _revealStyleParameters() {');
|
const sliders = sourceBlock(' setStyleParameter(name, value) {', ' _revealStyleParameters() {');
|
||||||
assertClaimsBefore(sliders, 'this.stages[styleName].uniforms[uName] = val', 'style parameter slider');
|
assertClaimsBefore(sliders, 'this.stages[parameters.style].uniforms[name] = value', 'style parameter slider');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('public visual facades reject the complete invalid request before authority or mutation', () => {
|
test('public visual facades reject the complete invalid request before authority or mutation', () => {
|
||||||
@@ -794,7 +794,7 @@ test('visual input listeners are revoked before asynchronous UI teardown', () =>
|
|||||||
assert.match(LocationNavigation.prototype.destroy.toString(), /this\._locationLookup\?\.destroy\(\)/);
|
assert.match(LocationNavigation.prototype.destroy.toString(), /this\._locationLookup\?\.destroy\(\)/);
|
||||||
assert.match(DisplayBindings.prototype.destroy.toString(), /this\._detachDisplayControls\?\.\(\)/);
|
assert.match(DisplayBindings.prototype.destroy.toString(), /this\._detachDisplayControls\?\.\(\)/);
|
||||||
assert.match(DisplayBindings.prototype.destroy.toString(), /this\._applicationShortcuts\?\.destroy\(\)/);
|
assert.match(DisplayBindings.prototype.destroy.toString(), /this\._applicationShortcuts\?\.destroy\(\)/);
|
||||||
assert.match(VisualSettings.prototype.stop.toString(), /this\._styleParameters\?\.destroy\(\)/);
|
assert.match(VisualSettings.prototype.stop.toString(), /this\.store\.detach\(this\._parametersToken\)/);
|
||||||
assert.match(VisualSettings.prototype.stop.toString(), /this\._visualEffects\.stop\(\)/);
|
assert.match(VisualSettings.prototype.stop.toString(), /this\._visualEffects\.stop\(\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -225,7 +225,6 @@ export class StyleManager extends ShellFacade {
|
|||||||
},
|
},
|
||||||
elements: {
|
elements: {
|
||||||
_ppToggles: this._ppToggles,
|
_ppToggles: this._ppToggles,
|
||||||
_sliderContainer: this._sliderContainer,
|
|
||||||
_sliderPanel: this._sliderPanel,
|
_sliderPanel: this._sliderPanel,
|
||||||
},
|
},
|
||||||
operations: {
|
operations: {
|
||||||
|
|||||||
@@ -460,3 +460,118 @@ test('the imported files render what the file import publishes and keep focus',
|
|||||||
assert.equal(list.className, 'geo-import-list');
|
assert.equal(list.className, 'geo-import-list');
|
||||||
assert.equal(document.activeElement === $('geo-import-button'), true);
|
assert.equal(document.activeElement === $('geo-import-button'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the parameters panel shows the active style’s rows and hands each change to its owner', async (t) => {
|
||||||
|
const { store, $ } = fixture(t);
|
||||||
|
const panel = $('param-slider-panel');
|
||||||
|
assert.equal(panel.classList.contains('active'), false);
|
||||||
|
assert.equal($('param-sliders').children.length, 0);
|
||||||
|
const changes = [];
|
||||||
|
store.connect({
|
||||||
|
setStyleParameter: (name, value) => changes.push([name, value]),
|
||||||
|
});
|
||||||
|
store.publishParameters({
|
||||||
|
style: 'noir',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
label: 'Amount',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: '0.01',
|
||||||
|
digits: 2,
|
||||||
|
value: 0.25,
|
||||||
|
text: '0.25',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(panel.classList.contains('active'), true);
|
||||||
|
const row = $('param-sliders').querySelector('.param-slider-row');
|
||||||
|
assert.equal(row.querySelector('.param-label').textContent, 'Amount');
|
||||||
|
assert.equal(row.querySelector('.param-value').textContent, '0.25');
|
||||||
|
const slider = row.querySelector('input.param-slider');
|
||||||
|
assert.equal(slider.getAttribute('aria-label'), 'Amount');
|
||||||
|
assert.equal(slider.step, '0.01');
|
||||||
|
slider.value = '0.5';
|
||||||
|
slider.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
assert.deepEqual(changes, [['amount', 0.5]]);
|
||||||
|
store.publishParameters(null);
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(panel.classList.contains('active'), false);
|
||||||
|
assert.equal($('param-sliders').children.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('VisualSettings publishes a style’s parameters and writes a moved slider’s uniform', async (t) => {
|
||||||
|
const { store, $ } = fixture(t);
|
||||||
|
const owner = Object.create(VisualSettings.prototype);
|
||||||
|
const renders = [];
|
||||||
|
let shared = 0;
|
||||||
|
Object.assign(owner, {
|
||||||
|
store,
|
||||||
|
_visualEffects: {
|
||||||
|
stages: {
|
||||||
|
noir: {
|
||||||
|
uniforms: { contrastAmt: 1.2, grainAmt: 0.5, vignetteAmt: 0.5 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stop() {},
|
||||||
|
},
|
||||||
|
services: { governorRequestRender: (reason) => renders.push(reason) },
|
||||||
|
_parameters: null,
|
||||||
|
readShareLinks: () => null,
|
||||||
|
_scheduleRightPanelLayout() {},
|
||||||
|
_syncShareState: () => (shared += 1),
|
||||||
|
});
|
||||||
|
owner._parametersToken = store.connect({
|
||||||
|
setStyleParameter: (name, value) => owner.setStyleParameter(name, value),
|
||||||
|
});
|
||||||
|
owner._updateSliderPanel('noir');
|
||||||
|
await nextTick();
|
||||||
|
const labels = () =>
|
||||||
|
[...$('param-sliders').querySelectorAll('.param-label')].map(
|
||||||
|
(label) => label.textContent,
|
||||||
|
);
|
||||||
|
assert.deepEqual(labels(), ['Contrast', 'Grain', 'Vignette']);
|
||||||
|
const grain = $('param-sliders').querySelectorAll('input.param-slider')[1];
|
||||||
|
grain.value = '0.8';
|
||||||
|
grain.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(owner.stages.noir.uniforms.grainAmt, 0.8);
|
||||||
|
assert.equal(
|
||||||
|
$('param-sliders').querySelectorAll('.param-value')[1].textContent,
|
||||||
|
'0.80',
|
||||||
|
);
|
||||||
|
assert.deepEqual(renders, ['style-param-slider']);
|
||||||
|
assert.equal(shared, 1);
|
||||||
|
// A name that isn't one of the shown style's parameters writes nothing.
|
||||||
|
owner.setStyleParameter('pixelation', 4);
|
||||||
|
assert.equal(owner.stages.noir.uniforms.pixelation, undefined);
|
||||||
|
assert.equal(shared, 1);
|
||||||
|
owner._updateSliderPanel('normal');
|
||||||
|
await nextTick();
|
||||||
|
assert.deepEqual(labels(), []);
|
||||||
|
assert.equal($('param-slider-panel').classList.contains('active'), false);
|
||||||
|
// A preset that resets the shown style's uniforms republishes its rows.
|
||||||
|
owner._updateSliderPanel('noir');
|
||||||
|
owner.stages.noir.uniforms.grainAmt = 0.3;
|
||||||
|
owner._refreshParameters();
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(
|
||||||
|
$('param-sliders').querySelectorAll('.param-value')[1].textContent,
|
||||||
|
'0.30',
|
||||||
|
);
|
||||||
|
owner._lifetime = { destroy() {} };
|
||||||
|
owner.stop();
|
||||||
|
await nextTick();
|
||||||
|
assert.equal(store.parameters, null, 'the sliders go with their owner');
|
||||||
|
assert.equal($('param-slider-panel').classList.contains('active'), false);
|
||||||
|
assert.equal(store.act('setStyleParameter', 'grainAmt', 0.2), undefined);
|
||||||
|
assert.equal(owner.stages.noir.uniforms.grainAmt, 0.3, 'a stopped owner no longer answers');
|
||||||
|
// An older owner's stop leaves a newer owner's rows.
|
||||||
|
const newer = { style: 'noir', rows: [] };
|
||||||
|
owner._parameters = { style: 'noir', rows: [] };
|
||||||
|
store.publishParameters(newer);
|
||||||
|
owner.stop();
|
||||||
|
assert.equal(store.parameters, newer);
|
||||||
|
});
|
||||||
@@ -3,7 +3,6 @@ export function readShellElements(document = globalThis.document) {
|
|||||||
return {
|
return {
|
||||||
_rightPanelStack: document.getElementById('right-context-rail'),
|
_rightPanelStack: document.getElementById('right-context-rail'),
|
||||||
_sliderPanel: document.getElementById('param-slider-panel'),
|
_sliderPanel: document.getElementById('param-slider-panel'),
|
||||||
_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'),
|
||||||
_bloomSlider: document.getElementById('bloom-intensity-slider'),
|
_bloomSlider: document.getElementById('bloom-intensity-slider'),
|
||||||
|
|||||||
@@ -413,14 +413,6 @@ export class ShellFacade {
|
|||||||
this._visualSettings._detectionAllocationPreference = value;
|
this._visualSettings._detectionAllocationPreference = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get _styleParameters() {
|
|
||||||
return this._visualSettings._styleParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
set _styleParameters(value) {
|
|
||||||
this._visualSettings._styleParameters = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
get _irBoostActive() {
|
get _irBoostActive() {
|
||||||
return this._visualSettings._irBoostActive;
|
return this._visualSettings._irBoostActive;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,17 @@ export const useDisplayStore = defineStore('gev-display', () => {
|
|||||||
// buttons, the draw tool and the file import their own.
|
// buttons, the draw tool and the file import their own.
|
||||||
const { connect, detach, act } = createOwners();
|
const { connect, detach, act } = createOwners();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The active style's parameter rows, `{style, rows}`; null while the style
|
||||||
|
* has none. Each row's change calls act('setStyleParameter', name, value).
|
||||||
|
*/
|
||||||
|
const parameters = shallowRef(null);
|
||||||
|
|
||||||
|
/** @param {?{style: string, rows: object[]}} next */
|
||||||
|
function publishParameters(next) {
|
||||||
|
parameters.value = next && markRaw(next);
|
||||||
|
}
|
||||||
|
|
||||||
/** The phone shell's tab bar; null until the shell first presents it. */
|
/** The phone shell's tab bar; null until the shell first presents it. */
|
||||||
const phoneShell = shallowRef(null);
|
const phoneShell = shallowRef(null);
|
||||||
|
|
||||||
@@ -91,6 +102,8 @@ export const useDisplayStore = defineStore('gev-display', () => {
|
|||||||
publishGeoImport,
|
publishGeoImport,
|
||||||
style,
|
style,
|
||||||
publishStyle,
|
publishStyle,
|
||||||
|
parameters,
|
||||||
|
publishParameters,
|
||||||
phoneShell,
|
phoneShell,
|
||||||
publishPhoneShell,
|
publishPhoneShell,
|
||||||
mapSource,
|
mapSource,
|
||||||
|
|||||||
+69
-25
@@ -13,9 +13,9 @@ const formatParam = (value, digits) =>
|
|||||||
/**
|
/**
|
||||||
* The generated rows: a label, the kit's slider and its value in the UI
|
* The generated rows: a label, the kit's slider and its value in the UI
|
||||||
* language. A render function, so the portable input package needs no
|
* language. A render function, so the portable input package needs no
|
||||||
* single-file-component compiler.
|
* single-file-component compiler. The Display panel renders it too.
|
||||||
*/
|
*/
|
||||||
const StyleParameterRows = {
|
export const StyleParameterRows = {
|
||||||
props: {
|
props: {
|
||||||
rows: { type: Array, required: true },
|
rows: { type: Array, required: true },
|
||||||
onInput: { type: Function, required: true },
|
onInput: { type: Function, required: true },
|
||||||
@@ -39,6 +39,68 @@ const StyleParameterRows = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A value as a range input holds it: clamped to its bounds and snapped to
|
||||||
|
* its step from the minimum, as the browser snaps the slider.
|
||||||
|
* @param {number} value
|
||||||
|
* @param {{min: number, max: number, step: string}} row
|
||||||
|
*/
|
||||||
|
export function snapToStep(value, { min, max, step }) {
|
||||||
|
const size = Number(step);
|
||||||
|
const decimals = (String(step).split('.')[1] || '').length;
|
||||||
|
// Rounded to nine places first, so a value halfway between two steps
|
||||||
|
// (1.15 from 1 by 0.1) rounds up as the browser does, not down through
|
||||||
|
// floating point error.
|
||||||
|
const count = (span) => Number((span / size).toFixed(9));
|
||||||
|
// A maximum off the step grid holds the last step below it, as the browser
|
||||||
|
// does.
|
||||||
|
const steps = Math.min(
|
||||||
|
Math.floor(count(max - min)),
|
||||||
|
Math.max(0, Math.round(count(Number(value) - min))),
|
||||||
|
);
|
||||||
|
return Number((min + steps * size).toFixed(decimals));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parameter rows of a style's uniforms: each one's label, bounds, step,
|
||||||
|
* value and the value's text in the UI language.
|
||||||
|
* @param {object} uniforms The style's uniform metadata.
|
||||||
|
* @param {(name: string) => number} readValue A uniform's current value.
|
||||||
|
*/
|
||||||
|
export function styleParameterRows(uniforms, readValue) {
|
||||||
|
return Object.entries(uniforms).map(([name, metadata]) =>
|
||||||
|
withValue(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
label: t(metadata.label),
|
||||||
|
min: metadata.min,
|
||||||
|
max: metadata.max,
|
||||||
|
step: metadata.max <= 1 ? '0.01' : '0.1',
|
||||||
|
digits: metadata.max <= 1 ? 2 : 1,
|
||||||
|
},
|
||||||
|
readValue(name),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rows with one parameter's value changed.
|
||||||
|
* @param {object[]} rows From {@link styleParameterRows}.
|
||||||
|
* @param {string} name
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
export function withParameterValue(rows, name, value) {
|
||||||
|
return rows.map((row) => (row.name === name ? withValue(row, value) : row));
|
||||||
|
}
|
||||||
|
|
||||||
|
function withValue(row, value) {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
value,
|
||||||
|
text: formatParam(snapToStep(value, row), row.digits),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Own generated parameter rows and their input handling. Renders
|
* Own generated parameter rows and their input handling. Renders
|
||||||
* StyleParameterRows into `container` and patches it synchronously.
|
* StyleParameterRows into `container` and patches it synchronously.
|
||||||
@@ -47,6 +109,9 @@ const StyleParameterRows = {
|
|||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @param {HTMLElement} options.container Container owned by this component.
|
* @param {HTMLElement} options.container Container owned by this component.
|
||||||
* @returns {{render: Function, clear: Function, destroy: Function}}
|
* @returns {{render: Function, clear: Function, destroy: Function}}
|
||||||
|
* @deprecated The Display panel renders the rows VisualSettings publishes to
|
||||||
|
* the display store (styleParameterRows); this stays exported from
|
||||||
|
* `./ui/input` for hosts that imported it.
|
||||||
*/
|
*/
|
||||||
export function createStyleParameters({ container }) {
|
export function createStyleParameters({ container }) {
|
||||||
let rows = [];
|
let rows = [];
|
||||||
@@ -69,36 +134,15 @@ export function createStyleParameters({ container }) {
|
|||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
clear();
|
clear();
|
||||||
const current = generation;
|
const current = generation;
|
||||||
rows = Object.entries(uniforms).map(([name, metadata]) => ({
|
rows = styleParameterRows(uniforms, readValue);
|
||||||
name,
|
|
||||||
label: t(metadata.label),
|
|
||||||
min: metadata.min,
|
|
||||||
max: metadata.max,
|
|
||||||
step: metadata.max <= 1 ? '0.01' : '0.1',
|
|
||||||
digits: metadata.max <= 1 ? 2 : 1,
|
|
||||||
value: readValue(name),
|
|
||||||
text: '',
|
|
||||||
}));
|
|
||||||
onInput = (name, value) => {
|
onInput = (name, value) => {
|
||||||
if (destroyed || current !== generation) return;
|
if (destroyed || current !== generation) return;
|
||||||
writeValue(name, value);
|
writeValue(name, value);
|
||||||
rows = rows.map((row) =>
|
rows = withParameterValue(rows, name, value);
|
||||||
row.name === name
|
|
||||||
? { ...row, value, text: formatParam(value, row.digits) }
|
|
||||||
: row,
|
|
||||||
);
|
|
||||||
paint();
|
paint();
|
||||||
onChange();
|
onChange();
|
||||||
};
|
};
|
||||||
paint();
|
paint();
|
||||||
// The readout shows the value the slider holds once the browser has
|
|
||||||
// clamped and snapped it to its step.
|
|
||||||
const sliders = container.querySelectorAll('input.param-slider');
|
|
||||||
rows = rows.map((row, index) => ({
|
|
||||||
...row,
|
|
||||||
text: formatParam(parseFloat(sliders[index].value), row.digits),
|
|
||||||
}));
|
|
||||||
paint();
|
|
||||||
},
|
},
|
||||||
clear,
|
clear,
|
||||||
destroy() {
|
destroy() {
|
||||||
|
|||||||
@@ -3,7 +3,12 @@
|
|||||||
import '../testSupport/registerDom.mjs';
|
import '../testSupport/registerDom.mjs';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { createStyleParameters } from './styleParameters.js';
|
import {
|
||||||
|
createStyleParameters,
|
||||||
|
snapToStep,
|
||||||
|
styleParameterRows,
|
||||||
|
withParameterValue,
|
||||||
|
} from './styleParameters.js';
|
||||||
|
|
||||||
function parameters(t, values = { amount: 0.25, scale: 2 }) {
|
function parameters(t, values = { amount: 0.25, scale: 2 }) {
|
||||||
document.body.replaceChildren();
|
document.body.replaceChildren();
|
||||||
@@ -120,3 +125,44 @@ test('destroy is final and cannot clear a subsequent owner’s DOM', (t) => {
|
|||||||
assert.equal(f.container.firstElementChild === nextOwner, true);
|
assert.equal(f.container.firstElementChild === nextOwner, true);
|
||||||
assert.deepEqual(f.order, []);
|
assert.deepEqual(f.order, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a value snaps to its step from the minimum and stays in bounds', () => {
|
||||||
|
const row = { min: 0, max: 1, step: '0.01' };
|
||||||
|
assert.equal(snapToStep(0.256, row), 0.26);
|
||||||
|
assert.equal(snapToStep(1.4, row), 1);
|
||||||
|
assert.equal(snapToStep(-2, row), 0);
|
||||||
|
assert.equal(snapToStep(2.04, { min: 0.5, max: 4, step: '0.1' }), 2);
|
||||||
|
// Halfway between two steps rounds up, as the browser snaps it.
|
||||||
|
assert.equal(snapToStep(1.15, { min: 1, max: 2, step: '0.1' }), 1.2);
|
||||||
|
assert.equal(snapToStep(0.125, { min: 0, max: 1, step: '0.01' }), 0.13);
|
||||||
|
// A maximum off the grid snaps down to the last step within it.
|
||||||
|
assert.equal(snapToStep(1.06, { min: 0, max: 1.06, step: '0.1' }), 1);
|
||||||
|
assert.equal(snapToStep(9, { min: 0, max: 1.06, step: '0.1' }), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rows carry each uniform, its step and its snapped value text', () => {
|
||||||
|
const rows = styleParameterRows(
|
||||||
|
{
|
||||||
|
amount: { label: 'Amount', min: 0, max: 1 },
|
||||||
|
scale: { label: 'Scale', min: 0.5, max: 4 },
|
||||||
|
},
|
||||||
|
(name) => ({ amount: 0.256, scale: 2.04 })[name],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
rows.map(({ name, step, digits, value, text }) => [
|
||||||
|
name,
|
||||||
|
step,
|
||||||
|
digits,
|
||||||
|
value,
|
||||||
|
text,
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
['amount', '0.01', 2, 0.256, '0.26'],
|
||||||
|
['scale', '0.1', 1, 2.04, '2.0'],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const next = withParameterValue(rows, 'scale', 3.33);
|
||||||
|
assert.equal(next[1].value, 3.33);
|
||||||
|
assert.equal(next[1].text, '3.3');
|
||||||
|
assert.equal(next[0], rows[0], 'the other rows are kept');
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { t } from '../../../i18n/index.js';
|
import { t } from '../../../i18n/index.js';
|
||||||
import { vPanelState } from '../../rail/panelState.js';
|
import { vPanelState } from '../../rail/panelState.js';
|
||||||
|
import { vClassToggle } from '../../rail/classToggle.js';
|
||||||
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
import PanelCollapseButton from '../../rail/PanelCollapseButton.vue';
|
||||||
import { vRangeValue } from '../../rail/rangeValue.js';
|
import { vRangeValue } from '../../rail/rangeValue.js';
|
||||||
import UiSegmented from '../../kit/UiSegmented.vue';
|
import UiSegmented from '../../kit/UiSegmented.vue';
|
||||||
@@ -11,13 +12,14 @@ import GeoImport from './GeoImport.vue';
|
|||||||
import { useDisplayStore } from '../../stores/display.js';
|
import { useDisplayStore } from '../../stores/display.js';
|
||||||
import UiButton from '../../kit/UiButton.vue';
|
import UiButton from '../../kit/UiButton.vue';
|
||||||
import UiSelect from '../../kit/UiSelect.js';
|
import UiSelect from '../../kit/UiSelect.js';
|
||||||
|
import { StyleParameterRows } from '../../styleParameters.js';
|
||||||
|
|
||||||
// The Display panel. VisualSettings presents the HUD, Sonar, Scope,
|
// The Display panel. VisualSettings presents the HUD, Sonar, Scope,
|
||||||
// Celestial, detection, bloom and sharpen, AircraftDisplay the 3D group and
|
// Celestial, detection, bloom and sharpen, AircraftDisplay the 3D group and
|
||||||
// the draw tool its controls, and each publishes them; until they do, the
|
// the draw tool its controls, and each publishes them; until they do, the
|
||||||
// panel shows its initial markup. GeoImport renders the file import.
|
// panel shows its initial markup. GeoImport renders the file import.
|
||||||
const display = useDisplayStore();
|
const display = useDisplayStore();
|
||||||
const { view, models3d, draw } = storeToRefs(display);
|
const { view, models3d, draw, parameters } = storeToRefs(display);
|
||||||
// A control's change goes to its owner (DisplayBindings or the draw tool).
|
// A control's change goes to its owner (DisplayBindings or the draw tool).
|
||||||
const act = (name, ...args) => display.act(name, ...args);
|
const act = (name, ...args) => display.act(name, ...args);
|
||||||
const pct = (value) => `${value}%`;
|
const pct = (value) => `${value}%`;
|
||||||
@@ -131,14 +133,14 @@ const allocationOptions = [
|
|||||||
<span class="pp-slider-value" id="detection-opacity-value">{{ pct(detection?.outsideOpacityPct ?? 1) }}</span>
|
<span class="pp-slider-value" id="detection-opacity-value">{{ pct(detection?.outsideOpacityPct ?? 1) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- The one shared preset-parameter surface is inserted here at runtime. -->
|
<!-- The one shared preset-parameter surface: the active style's parameters, as VisualSettings publishes them. -->
|
||||||
<div id="param-slider-panel" v-panel-state="'param-slider-panel'">
|
<div id="param-slider-panel" v-panel-state="'param-slider-panel'" v-class-toggle="{ active: !!parameters }">
|
||||||
<div class="param-panel-header">
|
<div class="param-panel-header">
|
||||||
<span class="param-panel-title" data-i18n>PARAMETERS</span>
|
<span class="param-panel-title" data-i18n>PARAMETERS</span>
|
||||||
<span class="param-panel-divider"></span>
|
<span class="param-panel-divider"></span>
|
||||||
<PanelCollapseButton id="param-panel-collapse-btn" target="param-slider-panel" />
|
<PanelCollapseButton id="param-panel-collapse-btn" target="param-slider-panel" />
|
||||||
</div>
|
</div>
|
||||||
<div id="param-sliders"></div>
|
<div id="param-sliders"><StyleParameterRows v-if="parameters" :key="parameters.style" :rows="parameters.rows" :on-input="(name, value) => act('setStyleParameter', name, value)" /></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-toggle-group">
|
<div class="pp-toggle-group">
|
||||||
<!-- 3D aircraft is DEFAULT-ON in Proximity (owner directive 2026-08-22), so the
|
<!-- 3D aircraft is DEFAULT-ON in Proximity (owner directive 2026-08-22), so the
|
||||||
|
|||||||
+50
-30
@@ -12,7 +12,7 @@ import {
|
|||||||
STYLE_PRESET_DEFAULTS,
|
STYLE_PRESET_DEFAULTS,
|
||||||
MILITARY_DETECTION_PRESET,
|
MILITARY_DETECTION_PRESET,
|
||||||
} from './effects.js';
|
} from './effects.js';
|
||||||
import { createStyleParameters } from './visualInput.js';
|
import { styleParameterRows, withParameterValue } from './styleParameters.js';
|
||||||
import {
|
import {
|
||||||
BLOOM_SCALE_VERSION,
|
BLOOM_SCALE_VERSION,
|
||||||
clampBloomIntensity,
|
clampBloomIntensity,
|
||||||
@@ -180,6 +180,11 @@ export class VisualSettings {
|
|||||||
readDisplayPortalActive,
|
readDisplayPortalActive,
|
||||||
});
|
});
|
||||||
this._lifetime = new UiLifetime();
|
this._lifetime = new UiLifetime();
|
||||||
|
// The active style's published parameter rows; null while it has none.
|
||||||
|
this._parameters = null;
|
||||||
|
this._parametersToken = store.connect({
|
||||||
|
setStyleParameter: (name, value) => this.setStyleParameter(name, value),
|
||||||
|
});
|
||||||
this._visualEffects = new VisualEffects({
|
this._visualEffects = new VisualEffects({
|
||||||
viewer,
|
viewer,
|
||||||
requestRender: services.governorRequestRender,
|
requestRender: services.governorRequestRender,
|
||||||
@@ -435,11 +440,7 @@ export class VisualSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_revealCockpitStyleParameters({ openDisplay = false } = {}) {
|
_revealCockpitStyleParameters({ openDisplay = false } = {}) {
|
||||||
if (
|
if (!this.cockpitView?.active || !this._parameters) return;
|
||||||
!this.cockpitView?.active ||
|
|
||||||
!this._sliderPanel?.classList.contains('active')
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
if (openDisplay && !this._cockpitDisclosureOpen?.('display')) {
|
if (openDisplay && !this._cockpitDisclosureOpen?.('display')) {
|
||||||
this._setCockpitDisclosure?.('display', true);
|
this._setCockpitDisclosure?.('display', true);
|
||||||
return;
|
return;
|
||||||
@@ -763,6 +764,7 @@ export class VisualSettings {
|
|||||||
governorRequestRender('style-param');
|
governorRequestRender('style-param');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._refreshParameters();
|
||||||
}
|
}
|
||||||
|
|
||||||
const bloomInput = preset.bloom || {};
|
const bloomInput = preset.bloom || {};
|
||||||
@@ -1397,41 +1399,54 @@ export class VisualSettings {
|
|||||||
this._syncShareState();
|
this._syncShareState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Publish the style's parameter rows, or none for a style without any. */
|
||||||
_updateSliderPanel(styleName, { reveal = false } = {}) {
|
_updateSliderPanel(styleName, { reveal = false } = {}) {
|
||||||
const { governorRequestRender } = this.services;
|
|
||||||
this._styleParameters ||= createStyleParameters({
|
|
||||||
container: this._sliderContainer,
|
|
||||||
});
|
|
||||||
this._styleParameters.clear();
|
|
||||||
const shader = STYLES[styleName];
|
const shader = STYLES[styleName];
|
||||||
|
|
||||||
if (!shader || !shader.uniforms || styleName === 'normal') {
|
if (!shader || !shader.uniforms || styleName === 'normal') {
|
||||||
this._sliderPanel.classList.remove('active');
|
this._publishParameters(null);
|
||||||
this._scheduleRightPanelLayout();
|
this._scheduleRightPanelLayout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._publishParameters({
|
||||||
this._styleParameters.render({
|
style: styleName,
|
||||||
uniforms: shader.uniforms,
|
rows: styleParameterRows(
|
||||||
readValue: (uName) => this.stages[styleName].uniforms[uName],
|
shader.uniforms,
|
||||||
writeValue: (uName, val) => {
|
(name) => this.stages[styleName].uniforms[name],
|
||||||
this.shareLinkManager?.claimRestoreLane?.('visual');
|
),
|
||||||
this.stages[styleName].uniforms[uName] = val;
|
|
||||||
},
|
|
||||||
onChange: () => {
|
|
||||||
// Uniform writes need an explicit render under the idle governor.
|
|
||||||
governorRequestRender('style-param-slider');
|
|
||||||
this._syncShareState();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this._sliderPanel.classList.add('active');
|
|
||||||
this._scheduleRightPanelLayout();
|
this._scheduleRightPanelLayout();
|
||||||
if (reveal) this._revealStyleParameters();
|
if (reveal) this._revealStyleParameters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Publish the shown style's rows again from its uniforms as they stand. */
|
||||||
|
_refreshParameters() {
|
||||||
|
const style = this._parameters?.style;
|
||||||
|
if (style) this._updateSliderPanel(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
_publishParameters(next) {
|
||||||
|
this._parameters = next;
|
||||||
|
this.store.publishParameters(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A parameter slider moved: write the active style's uniform. */
|
||||||
|
setStyleParameter(name, value) {
|
||||||
|
const parameters = this._parameters;
|
||||||
|
// Only the shown style's own parameters.
|
||||||
|
if (!parameters?.rows.some((row) => row.name === name)) return;
|
||||||
|
this.shareLinkManager?.claimRestoreLane?.('visual');
|
||||||
|
this.stages[parameters.style].uniforms[name] = value;
|
||||||
|
this._publishParameters({
|
||||||
|
...parameters,
|
||||||
|
rows: withParameterValue(parameters.rows, name, value),
|
||||||
|
});
|
||||||
|
// Uniform writes need an explicit render under the idle governor.
|
||||||
|
this.services.governorRequestRender('style-param-slider');
|
||||||
|
this._syncShareState();
|
||||||
|
}
|
||||||
|
|
||||||
_revealStyleParameters() {
|
_revealStyleParameters() {
|
||||||
if (!this._sliderPanel?.classList.contains('active')) return;
|
if (!this._parameters) return;
|
||||||
if (this._cockpitDisplayPortalActive) return;
|
if (this._cockpitDisplayPortalActive) return;
|
||||||
this._presentPanel(this._sliderPanel, { collapsed: false });
|
this._presentPanel(this._sliderPanel, { collapsed: false });
|
||||||
this.setPanelCollapsed('pp-toggles', false, { explicit: true });
|
this.setPanelCollapsed('pp-toggles', false, { explicit: true });
|
||||||
@@ -1574,7 +1589,12 @@ export class VisualSettings {
|
|||||||
}
|
}
|
||||||
stop() {
|
stop() {
|
||||||
this._lifetime.destroy();
|
this._lifetime.destroy();
|
||||||
this._styleParameters?.destroy();
|
if (this._parametersToken) this.store.detach(this._parametersToken);
|
||||||
|
this._parametersToken = null;
|
||||||
|
// The sliders go with their owner, unless a newer owner has published
|
||||||
|
// its own since.
|
||||||
|
if (this._parameters && this.store.parameters === this._parameters)
|
||||||
|
this._publishParameters(null);
|
||||||
this._visualEffects.stop();
|
this._visualEffects.stop();
|
||||||
}
|
}
|
||||||
releaseIrBoost() {
|
releaseIrBoost() {
|
||||||
|
|||||||
Reference in new issue
Block a user