From d145c30967e14c23fb0da565142897b28fb1c847 Mon Sep 17 00:00:00 2001 From: Manjunath Paparaju Date: Sat, 14 Nov 2026 14:44:23 -0800 Subject: [PATCH 01/15] Add scope opacity and a reset to the Visual panel The Display panel is regrouped as Visual (Look, Overlay, Effects) with style presets, a scope opacity slider and a reset to the default look. Scope opacity is carried in share links as field 30 and as the legacy sco parameter; 100 is the default and is left out of a link. Co-authored-by: Sameh Khamis --- src/scopeMask.js | 21 ++ src/share/legacyShareState.js | 1 + src/share/viewCodec.js | 2 + src/share/viewCodec.test.mjs | 1 + src/sharelink.js | 9 + src/ui/composition.js | 4 + src/ui/cyberTheme.test.mjs | 6 +- src/ui/displayBindings.js | 19 ++ src/ui/styles/scope-off-shadows.css | 24 +++ src/ui/styles/visual-panel.css | 286 ++++++++++++++++++++++++++ src/ui/views/panels/DisplayPanel.vue | 27 ++- src/ui/views/panels/VisualPresets.vue | 66 ++++++ src/ui/visualSettings.js | 16 +- 13 files changed, 471 insertions(+), 11 deletions(-) create mode 100644 src/ui/styles/scope-off-shadows.css create mode 100644 src/ui/styles/visual-panel.css create mode 100644 src/ui/views/panels/VisualPresets.vue diff --git a/src/scopeMask.js b/src/scopeMask.js index c43108377..770539ca8 100644 --- a/src/scopeMask.js +++ b/src/scopeMask.js @@ -89,6 +89,12 @@ let _container = null; let _viewer = null; let _enabled = true; let _featherRatio = SCOPE_FEATHER_RATIO_DEFAULT; +/** + * How strongly the whole mask shows, from the Display panel's Opacity slider: + * a fade over the painted mask, separate from the altitude terminus above, + * so the terminus band and its repaint gate stay as they are. + */ +let _maskOpacity = 1; let _resizeObserver = null; let _dprQuery = null; let _dprListener = null; @@ -433,6 +439,7 @@ export function installScopeMask(viewer) { _canvas = document.createElement('canvas'); _canvas.id = 'scope-mask'; _canvas.setAttribute('aria-hidden', 'true'); + _canvas.style.opacity = String(_maskOpacity); _container.appendChild(_canvas); _resizeObserver = new ResizeObserver(() => draw()); _resizeObserver.observe(_container); @@ -537,6 +544,20 @@ export function getScopeMaskFeather() { return _featherRatio; } +/** + * @param {number} ratio - How strongly the mask shows, 0 (clear) to 1. + * @returns {void} + */ +export function setScopeMaskOpacity(ratio) { + _maskOpacity = Math.max(0, Math.min(1, Number.isFinite(Number(ratio)) ? Number(ratio) : 1)); + if (_canvas) _canvas.style.opacity = String(_maskOpacity); +} + +/** @returns {number} */ +export function getScopeMaskOpacity() { + return _maskOpacity; +} + /** * Tear the mask down (canvas + observer). Reinstall with installScopeMask. * @returns {void} diff --git a/src/share/legacyShareState.js b/src/share/legacyShareState.js index 391293afe..e941faeee 100644 --- a/src/share/legacyShareState.js +++ b/src/share/legacyShareState.js @@ -43,6 +43,7 @@ export const LEGACY_SHARE_STATE_KEYS = Object.freeze([ 'celestialRing', 'scopeEnabled', 'scopeFeatherPct', + 'scopeOpacityPct', 'scopeTerminusPct', 'mapStack', 'layerState', diff --git a/src/share/viewCodec.js b/src/share/viewCodec.js index fd8059799..a95255789 100644 --- a/src/share/viewCodec.js +++ b/src/share/viewCodec.js @@ -393,6 +393,7 @@ export const SHARE_VIEW_FIELDS = freezeAll([ // Data Library entries switched on, as their two-character ledger tokens // concatenated (src/library/ledger.json). Absent means none. { number: 29, key: 'library', type: 'bytes', default: null }, + { number: 30, key: 'scopeOpacityPct', type: 'uint', default: 100 }, ]); const FIELD_BY_KEY = new Map(SHARE_VIEW_FIELDS.map((f) => [f.key, f])); const fieldNumber = (key) => FIELD_BY_KEY.get(key).number; @@ -780,6 +781,7 @@ export function normalizeShareViewState(input = {}) { state.scopeFeatherPct = finite(source.scopeFeatherPct) ? clampScopeFeatherPct(Number(source.scopeFeatherPct)) : d.scopeFeatherPct; + state.scopeOpacityPct = intOr(source.scopeOpacityPct, d.scopeOpacityPct, 0, 100); state.scopeTerminusPct = intOr( source.scopeTerminusPct, null, diff --git a/src/share/viewCodec.test.mjs b/src/share/viewCodec.test.mjs index 42dd6b547..f19261a16 100644 --- a/src/share/viewCodec.test.mjs +++ b/src/share/viewCodec.test.mjs @@ -509,6 +509,7 @@ test('the field registry is well formed and permanent', () => { [27, 'styleParams', BY_STYLE], [28, 'worldTime', null], [29, 'library', null], + [30, 'scopeOpacityPct', 100], ], ); assert.deepEqual( diff --git a/src/sharelink.js b/src/sharelink.js index 6577afeae..3b873ef30 100644 --- a/src/sharelink.js +++ b/src/sharelink.js @@ -104,6 +104,9 @@ export class ShareLinkManager { this._celestialRingEnabled = false; this._scopeEnabled = true; this._scopeFeatherPct = 100; + // How strongly the scope mask shows, 0..100; 100 (the full mask) is the + // default and leaves `sco` out of the link. + this._scopeOpacityPct = 100; // null = the altitude-adaptive terminus (the default). A number pins the // outside-fill opacity as a percent, 94..100. (`sce`, 2026-08-17) this._scopeTerminusPct = null; @@ -286,6 +289,7 @@ export class ShareLinkManager { // mirrors live state for the link this session generates, so it must match // the mask, not the archive.) scopeFeatherPct: clampScopeFeatherPct(parseOr(params.get('scf'), 35)), + scopeOpacityPct: Math.max(0, Math.min(100, Math.round(parseOr(params.get('sco'), 100)))), // Absent (or non-numeric) `sce` = adaptive (null), the default behavior; // a value pins the terminus opacity percent, clamped into the SUPPORTED // 94..100 band. `sce=0` used to survive as a sub-94 terminus — a hole in @@ -459,6 +463,7 @@ export class ShareLinkManager { celestialRing: visualCurrent ? state.celestialRing : undefined, scopeEnabled: visualCurrent ? state.scopeEnabled : undefined, scopeFeatherPct: visualCurrent ? state.scopeFeatherPct : undefined, + scopeOpacityPct: visualCurrent ? state.scopeOpacityPct : undefined, scopeTerminusPct: visualCurrent ? state.scopeTerminusPct : undefined, mapStack: mapCurrent ? state.mapStack : undefined, panelState, @@ -625,6 +630,9 @@ export class ShareLinkManager { if (typeof extras.scopeFeatherPct === 'number') { this._scopeFeatherPct = clampScopeFeatherPct(extras.scopeFeatherPct); } + if (typeof extras.scopeOpacityPct === 'number') { + this._scopeOpacityPct = Math.max(0, Math.min(100, Math.round(extras.scopeOpacityPct))); + } if (extras.scopeTerminusPct === null) this._scopeTerminusPct = null; else if (typeof extras.scopeTerminusPct === 'number') { this._scopeTerminusPct = clampScopeTerminusPct(extras.scopeTerminusPct); @@ -740,6 +748,7 @@ export class ShareLinkManager { params.set('cr', this._celestialRingEnabled ? '1' : '0'); params.set('sc', this._scopeEnabled ? '1' : '0'); params.set('scf', Math.round(this._scopeFeatherPct).toString()); + if (this._scopeOpacityPct !== 100) params.set('sco', String(this._scopeOpacityPct)); // Only written when pinned — an absent `sce` IS the adaptive default, so a // shared link never freezes the ramp for the recipient by accident. The // same 94..100 clamp applies on the way OUT, so a link can never carry an diff --git a/src/ui/composition.js b/src/ui/composition.js index 3b95c1a8c..169e73e53 100644 --- a/src/ui/composition.js +++ b/src/ui/composition.js @@ -56,6 +56,8 @@ import { isScopeMaskEnabled, setScopeMaskFeather, getScopeMaskFeather, + setScopeMaskOpacity, + getScopeMaskOpacity, setScopeTerminusOverride, getScopeTerminusOverride, clampScopeTerminusPct, @@ -112,6 +114,8 @@ export class StyleManager extends ApplicationShell { isScopeMaskEnabled, setScopeMaskFeather, getScopeMaskFeather, + setScopeMaskOpacity, + getScopeMaskOpacity, setScopeTerminusOverride, getScopeTerminusOverride, clampScopeTerminusPct, diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs index 4fecad9e7..fdbe64284 100644 --- a/src/ui/cyberTheme.test.mjs +++ b/src/ui/cyberTheme.test.mjs @@ -282,7 +282,7 @@ test('scope choice and expanded feather survive every HUD layout and both restor let shown = null; owner.store = { publish: (view) => (shown = view.scope) }; await owner.restoreShareState({ scopeEnabled: false, scopeFeatherPct: 200 }); - assert.deepEqual(shown, { enabled: false, featherPct: 200 }); + assert.deepEqual(shown, { enabled: false, featherPct: 200, opacityPct: 100 }); for (const layout of ['minimal', 'cyber', 'tactical']) { owner._setHudVariant(layout, { applyVisualDefaults: true }); assert.equal(isScopeMaskEnabled(), false, layout); @@ -292,10 +292,10 @@ test('scope choice and expanded feather survive every HUD layout and both restor await owner.applyVisualState({ scope: { enabled: true, featherPct: 175 } }); assert.equal(isScopeMaskEnabled(), true); assert.equal(getScopeMaskFeather(), 1.75); - assert.deepEqual(shown, { enabled: true, featherPct: 175 }); + assert.deepEqual(shown, { enabled: true, featherPct: 175, opacityPct: 100 }); await owner.applyVisualState({ scope: { featherPct: 500 } }); assert.equal(getScopeMaskFeather(), 2); - assert.deepEqual(shown, { enabled: true, featherPct: 200 }); + assert.deepEqual(shown, { enabled: true, featherPct: 200, opacityPct: 100 }); }); test('a scene shot saved with the retired Snow style plays on Normal', async (t) => { diff --git a/src/ui/displayBindings.js b/src/ui/displayBindings.js index 73c4fbee6..40e4a1edb 100644 --- a/src/ui/displayBindings.js +++ b/src/ui/displayBindings.js @@ -105,6 +105,11 @@ export class DisplayBindings { this._setScope({ featherPct: value }); this._syncShareState(); }, + setScopeOpacity: (value) => { + this.shareLinkManager?.claimRestoreLane?.('visual'); + this._setScope({ opacityPct: Number(value) }); + this._syncShareState(); + }, setSharpenIntensity: (pct) => { this.shareLinkManager?.claimRestoreLane?.('visual'); this._setSharpenIntensity(pct); @@ -168,6 +173,20 @@ export class DisplayBindings { this._syncModels3dModeRow(); }, setModelsMode: (mode) => this._setModels3dMode(mode), + // Back to the out-of-the-box look: Normal style, the global post + // defaults (bloom, sharpen, HUD, labels, celestial), scope on at full + // feather and opacity, 3D aircraft on in Proximity. + resetVisual: () => { + this.shareLinkManager?.claimRestoreLane?.('visual'); + this.setStyle('normal', { userInitiated: true }); + this._applyGlobalPostDefaults(); + this._setDetectionAllocation('ELASTIC'); + this._setScope({ enabled: true, featherPct: 100, opacityPct: 100 }); + this._setModels3dEnabled(true); + this._setModels3dMode('proximity'); + this._syncModels3dModeRow?.(); + this._syncShareState(); + }, }); this._detachDisplayControls = () => store.detach(token); } diff --git a/src/ui/styles/scope-off-shadows.css b/src/ui/styles/scope-off-shadows.css new file mode 100644 index 000000000..779c8f70d --- /dev/null +++ b/src/ui/styles/scope-off-shadows.css @@ -0,0 +1,24 @@ +/* With Scope off the map fills the whole screen. The HUD keeps its black + plates (overlays.css); the glass chrome lifts off the map with a soft + drop shadow. */ +:root[data-scope-mask='off'] { + --scope-off-panel-shadow: + 0 10px 30px rgba(0, 0, 0, 0.55), + 0 2px 8px rgba(0, 0, 0, 0.45); +} + +/* Glass surfaces (rails, the rails' name panels, open panels and + the dock) get a lift so their edges hold against bright imagery. */ +:root[data-scope-mask='off'] .ui-rail, +:root[data-scope-mask='off'] .gev-panel-rail .ui-rail--expanded::after, +:root[data-scope-mask='off'] :is(.data-panel-inner, .utilities-panel-inner, .ui-panel__inner), +:root[data-scope-mask='off'] #left-panel-stack > #pp-toggles, +:root[data-scope-mask='off'] #right-context-rail > [data-panel-id]:not(.collapsed), +:root[data-scope-mask='off'] #command-dock > * { + box-shadow: var(--scope-off-panel-shadow); +} + +:root[data-scope-mask='off'] .ui-rail__label, +:root[data-scope-mask='off'] .ui-rail__item .ui-icon { + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); +} diff --git a/src/ui/styles/visual-panel.css b/src/ui/styles/visual-panel.css new file mode 100644 index 000000000..4511d0289 --- /dev/null +++ b/src/ui/styles/visual-panel.css @@ -0,0 +1,286 @@ +/* ── Visual panel: sections and one card style for every control ─────── + Look · Map source · Overlay (HUD, detection, 3D, Scope, Clean UI) · + Effects (Celestial, Bloom, Sharpen) · Reset. Each control is a card: a + header row with its icon, name and an on/off pill, and its settings + underneath. */ + +/* ── Presets and map source at the top of the panel ─────────────────── */ +.visual-presets { + display: grid; + gap: 8px; + padding: 2px 0 12px; + margin-bottom: 10px; + border-bottom: 1px solid var(--glass-border); +} + +.visual-presets__heading { + margin: 4px 0 0; + color: var(--text-secondary); + font: 600 10px/1.2 var(--font-mono); + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.visual-presets__heading--row { + display: flex; + justify-content: space-between; + gap: 8px; +} + +.visual-presets__status { + color: var(--accent); +} + +.visual-presets__status.warn { + color: #ff9f43; +} + +.visual-presets__grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; +} + +.visual-presets__style { + position: relative; + display: grid; + justify-items: center; + gap: 4px; + padding: 9px 4px 7px; + color: var(--text-secondary); + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--glass-border); + border-radius: var(--btn-radius); + cursor: pointer; +} + +.visual-presets__style:hover { + color: var(--text-primary); + background: rgba(255, 255, 255, 0.06); +} + +.visual-presets__style.active { + color: var(--accent); + background: var(--accent-dim); + border-color: var(--accent-dim); +} + +.visual-presets__icon { + font-size: 15px; + line-height: 1; +} + +.visual-presets__label { + font: 500 11px/1 var(--font-sans); +} + +.visual-presets__key { + position: absolute; + top: 4px; + inset-inline-end: 5px; + color: var(--text-secondary); + font: 9px/1 var(--font-mono); + opacity: 0.6; +} + +.visual-presets__chips { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.visual-presets__chip { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 9px; + color: var(--text-secondary); + font: 500 11px/1.2 var(--font-sans); + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--glass-border); + border-radius: 999px; + cursor: pointer; +} + +.visual-presets__chip small { + font-size: 9px; + opacity: 0.6; +} + +.visual-presets__chip:hover { + color: var(--text-primary); + background: rgba(255, 255, 255, 0.06); +} + +.visual-presets__chip.active { + color: var(--accent); + background: var(--accent-dim); + border-color: var(--accent-dim); +} + +.visual-presets__chip.unavailable { + opacity: 0.45; + cursor: default; +} + +/* The body is display: contents, so its children are the panel's own flex + items: keep them full width and unshrunk. */ +#pp-toggles .pp-panel-body > * { + flex: none !important; + align-self: stretch !important; + box-sizing: border-box; +} + +#pp-toggles .pp-panel-body > .pp-toggle-group, +#pp-toggles .pp-panel-body > .pp-toggle-btn { + margin-bottom: 0.4rem !important; +} + +#pp-toggles .pp-section-heading, +#pp-toggles .visual-presets__heading { + margin: 0.7rem 2px 0.35rem; + text-align: start; + color: rgba(199, 214, 222, 0.55); + font-family: var(--font-mono); + font-size: 0.56rem; + font-weight: 500; + letter-spacing: 0.14rem; + text-transform: uppercase; +} + +#pp-toggles .visual-presets__heading:first-child { + margin-top: 0.2rem; +} + +#pp-toggles .pp-toggle-group { + width: 100%; + overflow: hidden; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 10px; +} + +/* The header row of a card, and the lone Clean UI button. */ +#pp-toggles .pp-toggle-group > .pp-toggle-btn, +#pp-toggles .pp-panel-body > .pp-toggle-btn { + position: relative; + box-sizing: border-box; + width: 100%; + min-height: 38px; + padding: 0 12px; + gap: 10px; + background: transparent !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} + +#pp-toggles .pp-panel-body > .pp-toggle-btn { + background: rgba(255, 255, 255, 0.03) !important; + border: 1px solid rgba(255, 255, 255, 0.07) !important; + border-radius: 10px !important; +} + +#pp-toggles .pp-toggle-btn .pp-icon { + width: 18px; + font-size: 15px; + text-align: center; + color: rgba(199, 214, 222, 0.75); +} + +#pp-toggles .pp-toggle-btn .pp-label { + color: rgba(236, 244, 248, 0.9); + font-family: var(--font-mono); + font-size: 0.62rem; + letter-spacing: 0.12rem; + text-transform: uppercase; +} + +/* An on/off pill at the right of every button header (Scope keeps its + checkbox, which already says it). */ +#pp-toggles .pp-toggle-group > button.pp-toggle-btn::after, +#pp-toggles .pp-panel-body > button.pp-toggle-btn::after { + content: 'OFF'; + margin-inline-start: auto; + padding: 2px 8px; + color: rgba(199, 214, 222, 0.5); + font-family: var(--font-mono); + font-size: 0.5rem; + letter-spacing: 0.12rem; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 999px; +} + +#pp-toggles .pp-toggle-group > button.pp-toggle-btn.active::after, +#pp-toggles .pp-toggle-group > button.pp-toggle-btn[aria-pressed='true']::after, +#pp-toggles .pp-panel-body > button.pp-toggle-btn.active::after { + content: 'ON'; + color: #7af0ff; + border-color: rgba(54, 220, 255, 0.5); + background: rgba(54, 220, 255, 0.1); +} + +#pp-toggles .pp-toggle-btn.active .pp-icon, +#pp-toggles .pp-toggle-btn[aria-pressed='true'] .pp-icon, +#pp-toggles .pp-toggle-btn.active .pp-label, +#pp-toggles .pp-toggle-btn[aria-pressed='true'] .pp-label { + color: #7af0ff; +} + +#pp-toggles .pp-toggle-group > .pp-toggle-btn:hover, +#pp-toggles .pp-panel-body > .pp-toggle-btn:hover { + background: rgba(255, 255, 255, 0.04) !important; +} + +#pp-toggles .pp-scope-toggle input[type='checkbox'] { + margin-inline-start: auto; +} + +/* Settings inside a card: separated by hairlines, no boxes of their own. */ +#pp-toggles .pp-toggle-group .pp-slider-row.visible, +#pp-toggles .pp-toggle-group .pp-slider-row:not([class*='cyber-sonar']):is(.visible, #scope-slider-row, #scope-opacity-row) { + margin: 0; + padding: 7px 12px; + background: transparent; + border: 0; + border-top: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 0; +} + +/* The Dense button lights green elsewhere; in a card it uses the shared pill. */ +#pp-toggles .pp-toggle-group > .pp-toggle-btn.panoptic, +#pp-toggles .pp-toggle-group > .pp-toggle-btn.active { + color: inherit; +} + +/* Reset to default, at the foot of the Visual panel. */ +#pp-toggles .pp-reset-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + margin: 0.6rem 0 0.2rem !important; + padding: 0.55rem; + color: rgba(225, 236, 242, 0.85); + font-family: var(--font-mono); + font-size: 0.6rem; + letter-spacing: 0.12rem; + text-transform: uppercase; + background: transparent; + border: 1px dashed rgba(255, 255, 255, 0.18); + border-radius: 10px; + cursor: pointer; +} + +#pp-toggles .pp-reset-btn .material-symbols-outlined { + font-size: 15px; +} + +#pp-toggles .pp-reset-btn:hover, +#pp-toggles .pp-reset-btn:focus-visible { + color: #fff; + border-color: rgba(54, 220, 255, 0.5); + background: rgba(54, 220, 255, 0.06); +} diff --git a/src/ui/views/panels/DisplayPanel.vue b/src/ui/views/panels/DisplayPanel.vue index 3cd202161..695c3bc25 100644 --- a/src/ui/views/panels/DisplayPanel.vue +++ b/src/ui/views/panels/DisplayPanel.vue @@ -5,12 +5,14 @@ import { t } from '../../../i18n/index.js'; import { vPanelState } from '../../rail/panelState.js'; import { vClassToggle } from '../../rail/classToggle.js'; import PanelCollapseButton from '../../rail/PanelCollapseButton.vue'; +import UiPanelEdge from '../../kit/UiPanelEdge.vue'; import UiSegmented from '../../kit/UiSegmented.vue'; import UiSlider from '../../kit/UiSlider.js'; import { useDisplayStore } from '../../stores/display.js'; import UiButton from '../../kit/UiButton.vue'; import UiSelect from '../../kit/UiSelect.js'; import { StyleParameterRows } from '../../styleParameters.js'; +import VisualPresets from './VisualPresets.vue'; // The Display panel. VisualSettings presents the HUD, Sonar, Scope, // Celestial, detection, bloom and sharpen and AircraftDisplay the 3D group, @@ -29,6 +31,7 @@ const detectButton = computed(() => detection.value?.button ?? null); const allocation = computed(() => detection.value?.allocation ?? 'ELASTIC'); const scopeOn = computed(() => view.value?.scope.enabled ?? true); const scopeFeather = computed(() => view.value?.scope.featherPct ?? 100); +const scopeOpacity = computed(() => view.value?.scope.opacityPct ?? 100); const celestial = computed(() => view.value?.celestial ?? null); const celestialTitle = computed(() => celestial.value && !celestial.value.supported @@ -51,12 +54,15 @@ const allocationOptions = computed(() => [ + diff --git a/src/ui/views/panels/VisualPresets.vue b/src/ui/views/panels/VisualPresets.vue new file mode 100644 index 000000000..2905aae2b --- /dev/null +++ b/src/ui/views/panels/VisualPresets.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/ui/visualSettings.js b/src/ui/visualSettings.js index 736e4a49e..236d34816 100644 --- a/src/ui/visualSettings.js +++ b/src/ui/visualSettings.js @@ -66,6 +66,7 @@ export class VisualSettings { celestialRing, scopeEnabled, scopeFeatherPct, + scopeOpacityPct, scopeTerminusPct, mapStack, panelState, @@ -133,7 +134,11 @@ export class VisualSettings { focus: false, }); } - this._setScope({ enabled: scopeEnabled, featherPct: scopeFeatherPct }); + this._setScope({ + enabled: scopeEnabled, + featherPct: scopeFeatherPct, + opacityPct: typeof scopeOpacityPct === 'number' ? scopeOpacityPct : undefined, + }); // null restores the altitude-adaptive ramp; a number pins the terminus // (clamped to the supported 94..100 band, same as the `sce` hash key). if (scopeTerminusPct === null) setScopeTerminusOverride(null); @@ -479,6 +484,7 @@ export class VisualSettings { scope: { enabled: this._scopeEnabledShown, featherPct: this._scopeFeatherShown, + opacityPct: this._scopeOpacityShown ?? 100, }, celestial: this._celestialShown, }; @@ -488,7 +494,12 @@ export class VisualSettings { * Apply the Scope mask and show it on its controls; a field that is not * given stays as it is. `apply: false` only shows the mask's state. */ - _setScope({ enabled, featherPct } = {}, { apply = true } = {}) { + _setScope({ enabled, featherPct, opacityPct } = {}, { apply = true } = {}) { + if (typeof opacityPct === 'number') { + const pct = Math.max(0, Math.min(100, Math.round(opacityPct))); + if (apply) this.services.setScopeMaskOpacity?.(pct / 100); + this._scopeOpacityShown = pct; + } if (typeof enabled === 'boolean') { if (apply) this.services.setScopeMaskEnabled(enabled); this._scopeEnabledShown = enabled; @@ -903,6 +914,7 @@ export class VisualSettings { celestialRingEnabled: this.celestialRingEnabled, scopeEnabled: isScopeMaskEnabled(), scopeFeatherPct: Math.round(getScopeMaskFeather() * 100), + scopeOpacityPct: this._scopeOpacityShown ?? 100, // null when adaptive — the share layer omits `sce` entirely in that case. scopeTerminusPct: getScopeTerminusOverride() == null From 8379a0ffd3120bf2c49bf7e0f1505693706f38e6 Mon Sep 17 00:00:00 2001 From: Manjunath Paparaju Date: Sat, 14 Nov 2026 14:44:48 -0800 Subject: [PATCH 02/15] Wire scope opacity and the Visual reset into the shell The application shell passes the scope opacity setter and the global post defaults through to the Display bindings, the stylesheet imports the Visual panel and Scope-off styles, and the icon subset gains the reset icon. Co-authored-by: Sameh Khamis --- index.html | 2 +- src/ui/applicationShell.js | 2 ++ style.css | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 806a8b41b..24bc148b7 100644 --- a/index.html +++ b/index.html @@ -26,7 +26,7 @@ with the sources: src/materialSymbolsSubset.test.mjs fails when a source names a glyph this list is missing, because a missing glyph does not draw a box — the ligature never forms and the element renders the literal word. --> - +
diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index 564578e60..2e5af67d0 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -239,6 +239,7 @@ export class StyleManager extends ShellFacade { setKeyholeFadeTuning: services.setKeyholeFadeTuning, setScopeMaskEnabled: services.setScopeMaskEnabled, setScopeMaskFeather: services.setScopeMaskFeather, + setScopeMaskOpacity: services.setScopeMaskOpacity, }, elements: { _ppToggles: this._ppToggles, @@ -594,6 +595,7 @@ export class StyleManager extends ShellFacade { this._setDetectionAllocation(...args), _setDetectionFade: (...args) => this._setDetectionFade(...args), _setScope: (...args) => this._visualSettings._setScope(...args), + _applyGlobalPostDefaults: (...args) => this._applyGlobalPostDefaults(...args), setCelestialRingEnabled: (...args) => this.setCelestialRingEnabled(...args), _setModels3dEnabled: (...args) => this._setModels3dEnabled(...args), diff --git a/style.css b/style.css index 012714e4e..2d79212fb 100644 --- a/style.css +++ b/style.css @@ -49,5 +49,7 @@ @import './src/ui/styles/rail-split.css'; @import './src/ui/styles/left-tools.css'; @import './src/ui/styles/panel-edge.css'; +@import './src/ui/styles/visual-panel.css'; @import './src/ui/styles/ai-chat.css'; +@import './src/ui/styles/scope-off-shadows.css'; @import './src/ui/styles/cyber.css'; From 1dcc025832d88fc3259ac393fed2f14279ea7ae7 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 14 Nov 2026 14:46:19 -0800 Subject: [PATCH 03/15] Format the scope opacity changes Co-authored-by: Manjunath Paparaju --- src/scopeMask.js | 5 ++++- src/share/viewCodec.js | 7 ++++++- src/sharelink.js | 13 ++++++++++--- src/ui/applicationShell.js | 3 ++- src/ui/visualSettings.js | 3 ++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/scopeMask.js b/src/scopeMask.js index 770539ca8..510ea151a 100644 --- a/src/scopeMask.js +++ b/src/scopeMask.js @@ -549,7 +549,10 @@ export function getScopeMaskFeather() { * @returns {void} */ export function setScopeMaskOpacity(ratio) { - _maskOpacity = Math.max(0, Math.min(1, Number.isFinite(Number(ratio)) ? Number(ratio) : 1)); + _maskOpacity = Math.max( + 0, + Math.min(1, Number.isFinite(Number(ratio)) ? Number(ratio) : 1), + ); if (_canvas) _canvas.style.opacity = String(_maskOpacity); } diff --git a/src/share/viewCodec.js b/src/share/viewCodec.js index a95255789..1b444d2b9 100644 --- a/src/share/viewCodec.js +++ b/src/share/viewCodec.js @@ -781,7 +781,12 @@ export function normalizeShareViewState(input = {}) { state.scopeFeatherPct = finite(source.scopeFeatherPct) ? clampScopeFeatherPct(Number(source.scopeFeatherPct)) : d.scopeFeatherPct; - state.scopeOpacityPct = intOr(source.scopeOpacityPct, d.scopeOpacityPct, 0, 100); + state.scopeOpacityPct = intOr( + source.scopeOpacityPct, + d.scopeOpacityPct, + 0, + 100, + ); state.scopeTerminusPct = intOr( source.scopeTerminusPct, null, diff --git a/src/sharelink.js b/src/sharelink.js index 3b873ef30..5715d581c 100644 --- a/src/sharelink.js +++ b/src/sharelink.js @@ -289,7 +289,10 @@ export class ShareLinkManager { // mirrors live state for the link this session generates, so it must match // the mask, not the archive.) scopeFeatherPct: clampScopeFeatherPct(parseOr(params.get('scf'), 35)), - scopeOpacityPct: Math.max(0, Math.min(100, Math.round(parseOr(params.get('sco'), 100)))), + scopeOpacityPct: Math.max( + 0, + Math.min(100, Math.round(parseOr(params.get('sco'), 100))), + ), // Absent (or non-numeric) `sce` = adaptive (null), the default behavior; // a value pins the terminus opacity percent, clamped into the SUPPORTED // 94..100 band. `sce=0` used to survive as a sub-94 terminus — a hole in @@ -631,7 +634,10 @@ export class ShareLinkManager { this._scopeFeatherPct = clampScopeFeatherPct(extras.scopeFeatherPct); } if (typeof extras.scopeOpacityPct === 'number') { - this._scopeOpacityPct = Math.max(0, Math.min(100, Math.round(extras.scopeOpacityPct))); + this._scopeOpacityPct = Math.max( + 0, + Math.min(100, Math.round(extras.scopeOpacityPct)), + ); } if (extras.scopeTerminusPct === null) this._scopeTerminusPct = null; else if (typeof extras.scopeTerminusPct === 'number') { @@ -748,7 +754,8 @@ export class ShareLinkManager { params.set('cr', this._celestialRingEnabled ? '1' : '0'); params.set('sc', this._scopeEnabled ? '1' : '0'); params.set('scf', Math.round(this._scopeFeatherPct).toString()); - if (this._scopeOpacityPct !== 100) params.set('sco', String(this._scopeOpacityPct)); + if (this._scopeOpacityPct !== 100) + params.set('sco', String(this._scopeOpacityPct)); // Only written when pinned — an absent `sce` IS the adaptive default, so a // shared link never freezes the ramp for the recipient by accident. The // same 94..100 clamp applies on the way OUT, so a link can never carry an diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index 2e5af67d0..a7d6fa29f 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -595,7 +595,8 @@ export class StyleManager extends ShellFacade { this._setDetectionAllocation(...args), _setDetectionFade: (...args) => this._setDetectionFade(...args), _setScope: (...args) => this._visualSettings._setScope(...args), - _applyGlobalPostDefaults: (...args) => this._applyGlobalPostDefaults(...args), + _applyGlobalPostDefaults: (...args) => + this._applyGlobalPostDefaults(...args), setCelestialRingEnabled: (...args) => this.setCelestialRingEnabled(...args), _setModels3dEnabled: (...args) => this._setModels3dEnabled(...args), diff --git a/src/ui/visualSettings.js b/src/ui/visualSettings.js index 236d34816..2b67847cd 100644 --- a/src/ui/visualSettings.js +++ b/src/ui/visualSettings.js @@ -137,7 +137,8 @@ export class VisualSettings { this._setScope({ enabled: scopeEnabled, featherPct: scopeFeatherPct, - opacityPct: typeof scopeOpacityPct === 'number' ? scopeOpacityPct : undefined, + opacityPct: + typeof scopeOpacityPct === 'number' ? scopeOpacityPct : undefined, }); // null restores the altitude-adaptive ramp; a number pins the terminus // (clamped to the supported 94..100 band, same as the `sce` hash key). From 493e76401e6e40922cb06eabf816d8b0e934f95d Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 14 Nov 2026 14:49:18 -0800 Subject: [PATCH 04/15] Own the Visual panel's new files in the package boundaries and format scope Co-authored-by: Manjunath Paparaju --- scripts/format-scope.json | 4 +++- scripts/package-boundaries.json | 1 + src/ui/styles/scope-off-shadows.css | 10 ++++++---- src/ui/styles/visual-panel.css | 8 +++++++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 4136ddb8a..49f71deb8 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1459,5 +1459,7 @@ "src/ui/styles/left-tools.css", "src/ui/styles/panel-edge.css", "src/data/placementProvider.test.mjs", - "src/layers/vessels/placement.test.mjs" + "src/layers/vessels/placement.test.mjs", + "src/ui/styles/visual-panel.css", + "src/ui/styles/scope-off-shadows.css" ] diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 4aa4d4a7c..a48c717ea 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -102,6 +102,7 @@ "src/ui/stores/streetLevel.js", "src/ui/rail/RailChips.vue", "src/ui/views/panels/DisplayPanel.vue", + "src/ui/views/panels/VisualPresets.vue", "src/ui/views/panels/GeoImport.vue", "src/ui/views/panels/DrawControls.vue", "src/ui/views/panels/DrawMarkList.vue", diff --git a/src/ui/styles/scope-off-shadows.css b/src/ui/styles/scope-off-shadows.css index 779c8f70d..8ad2cdeee 100644 --- a/src/ui/styles/scope-off-shadows.css +++ b/src/ui/styles/scope-off-shadows.css @@ -3,17 +3,19 @@ drop shadow. */ :root[data-scope-mask='off'] { --scope-off-panel-shadow: - 0 10px 30px rgba(0, 0, 0, 0.55), - 0 2px 8px rgba(0, 0, 0, 0.45); + 0 10px 30px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.45); } /* Glass surfaces (rails, the rails' name panels, open panels and the dock) get a lift so their edges hold against bright imagery. */ :root[data-scope-mask='off'] .ui-rail, :root[data-scope-mask='off'] .gev-panel-rail .ui-rail--expanded::after, -:root[data-scope-mask='off'] :is(.data-panel-inner, .utilities-panel-inner, .ui-panel__inner), +:root[data-scope-mask='off'] + :is(.data-panel-inner, .utilities-panel-inner, .ui-panel__inner), :root[data-scope-mask='off'] #left-panel-stack > #pp-toggles, -:root[data-scope-mask='off'] #right-context-rail > [data-panel-id]:not(.collapsed), +:root[data-scope-mask='off'] + #right-context-rail + > [data-panel-id]:not(.collapsed), :root[data-scope-mask='off'] #command-dock > * { box-shadow: var(--scope-off-panel-shadow); } diff --git a/src/ui/styles/visual-panel.css b/src/ui/styles/visual-panel.css index 4511d0289..696c08797 100644 --- a/src/ui/styles/visual-panel.css +++ b/src/ui/styles/visual-panel.css @@ -240,7 +240,13 @@ /* Settings inside a card: separated by hairlines, no boxes of their own. */ #pp-toggles .pp-toggle-group .pp-slider-row.visible, -#pp-toggles .pp-toggle-group .pp-slider-row:not([class*='cyber-sonar']):is(.visible, #scope-slider-row, #scope-opacity-row) { +#pp-toggles + .pp-toggle-group + .pp-slider-row:not([class*='cyber-sonar']):is( + .visible, + #scope-slider-row, + #scope-opacity-row + ) { margin: 0; padding: 7px 12px; background: transparent; From 5383b470eedcd0923a7f54146ce018dabd26346e Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 14 Nov 2026 14:52:10 -0800 Subject: [PATCH 05/15] Translate the Visual panel and keep its name apart from the satellite class The panel's new headings, the scope opacity slider and the reset are translated in every language, and the presets' names and descriptions use the dock's translations. The cards' on/off pills take their words from the locale instead of fixed English. VISUAL, the panel's name on the rail and in its header, gets its own translation context: several languages had it as "visible", from the satellite class of the same English word. Co-authored-by: Manjunath Paparaju --- src/i18n/locales/ar.json | 10 +++++- src/i18n/locales/de.json | 10 +++++- src/i18n/locales/es.json | 10 +++++- src/i18n/locales/fr.json | 10 +++++- src/i18n/locales/he.json | 10 +++++- src/i18n/locales/it.json | 10 +++++- src/i18n/locales/ja.json | 10 +++++- src/i18n/locales/pt.json | 10 +++++- src/i18n/locales/ru.json | 10 +++++- src/i18n/locales/sv.json | 10 +++++- src/i18n/locales/uk.json | 10 +++++- src/i18n/locales/zh-CN.json | 10 +++++- src/i18n/locales/zh-Hant.json | 10 +++++- src/i18n/same-as-english.json | 5 +++ src/ui/PanelRailView.vue | 4 ++- src/ui/panelRailCatalog.js | 6 ++-- src/ui/styles/visual-panel.css | 4 +-- src/ui/views/panels/DisplayPanel.vue | 12 +++++-- src/ui/views/panels/VisualPresets.vue | 51 +++++++++++++++++++++++---- 19 files changed, 184 insertions(+), 28 deletions(-) diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index e882d6f29..894076638 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -3969,5 +3969,13 @@ "Funicular": "قطار جبلي مائل", "Gondola": "تلفريك", "Move panels back beside the rail": "إعادة اللوحات إلى جانب الشريط", - "Move panels over the rail, to the edge": "نقل اللوحات فوق الشريط إلى الحافة" + "Move panels over the rail, to the edge": "نقل اللوحات فوق الشريط إلى الحافة", + "panel|VISUAL": "المرئيات", + "Look": "المظهر", + "Overlay": "التراكب", + "Effects": "المؤثرات", + "Scope opacity": "عتامة المنظار", + "How dark the scope shades the view outside the keyhole": "مدى تعتيم المنظار للعرض خارج الفتحة", + "Reset to default": "إعادة التعيين إلى الافتراضي", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "إعادة المرئيات إلى إعداداتها الافتراضية: المظهر العادي وHUD والمنظار والتسميات والمؤثرات" } diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 4dbecb4d1..408ddbc3a 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -3621,5 +3621,13 @@ "Funicular": "Standseilbahn", "Gondola": "Gondelbahn", "Move panels back beside the rail": "Bedienfelder zurück neben die Leiste", - "Move panels over the rail, to the edge": "Bedienfelder über die Leiste an den Rand verschieben" + "Move panels over the rail, to the edge": "Bedienfelder über die Leiste an den Rand verschieben", + "panel|VISUAL": "DARSTELLUNG", + "Look": "Look", + "Overlay": "Overlay", + "Effects": "Effekte", + "Scope opacity": "Scope-Deckkraft", + "How dark the scope shades the view outside the keyhole": "Wie stark der Scope die Ansicht außerhalb des Schlüssellochs abdunkelt", + "Reset to default": "Auf Standard zurücksetzen", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Darstellung auf die Standardwerte zurücksetzen: normaler Look, HUD, Scope, Beschriftungen und Effekte" } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index f63dae030..a7d626a28 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -3708,5 +3708,13 @@ "Funicular": "Funicular", "Gondola": "Góndola", "Move panels back beside the rail": "Devolver los paneles junto a la barra", - "Move panels over the rail, to the edge": "Mover los paneles sobre la barra, hasta el borde" + "Move panels over the rail, to the edge": "Mover los paneles sobre la barra, hasta el borde", + "panel|VISUAL": "VISUAL", + "Look": "Aspecto", + "Overlay": "Superposición", + "Effects": "Efectos", + "Scope opacity": "Opacidad de la mirilla", + "How dark the scope shades the view outside the keyhole": "Cuánto oscurece la mirilla la vista fuera del ojo de cerradura", + "Reset to default": "Restablecer valores predeterminados", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Restablecer Visual a sus valores predeterminados: aspecto Normal, HUD, mirilla, etiquetas y efectos" } diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 8f1996cde..d580354a3 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -3708,5 +3708,13 @@ "Funicular": "Funiculaire", "Gondola": "Télécabine", "Move panels back beside the rail": "Remettre les panneaux à côté de la barre", - "Move panels over the rail, to the edge": "Déplacer les panneaux par-dessus la barre, jusqu’au bord" + "Move panels over the rail, to the edge": "Déplacer les panneaux par-dessus la barre, jusqu’au bord", + "panel|VISUAL": "VISUEL", + "Look": "Aspect", + "Overlay": "Surcouche", + "Effects": "Effets", + "Scope opacity": "Opacité de la lunette", + "How dark the scope shades the view outside the keyhole": "À quel point la lunette assombrit la vue hors de l'ouverture", + "Reset to default": "Rétablir les valeurs par défaut", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Rétablir les valeurs par défaut de Visuel : aspect Normal, HUD, lunette, libellés et effets" } diff --git a/src/i18n/locales/he.json b/src/i18n/locales/he.json index c848be0bd..661cf2fef 100644 --- a/src/i18n/locales/he.json +++ b/src/i18n/locales/he.json @@ -3708,5 +3708,13 @@ "Funicular": "פוניקולר", "Gondola": "רכבל", "Move panels back beside the rail": "החזרת הלוחות לצד הסרגל", - "Move panels over the rail, to the edge": "הזזת הלוחות מעל הסרגל, אל הקצה" + "Move panels over the rail, to the edge": "הזזת הלוחות מעל הסרגל, אל הקצה", + "panel|VISUAL": "חזותי", + "Look": "מראה", + "Overlay": "שכבת על", + "Effects": "אפקטים", + "Scope opacity": "אטימות הסקופ", + "How dark the scope shades the view outside the keyhole": "עד כמה הסקופ מכהה את התצוגה מחוץ לחור המנעול", + "Reset to default": "איפוס לברירת המחדל", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "איפוס החזותי לברירות המחדל: מראה רגיל, HUD, סקופ, תוויות ואפקטים" } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index d93a963a5..cbd65beb7 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -3708,5 +3708,13 @@ "Funicular": "Funicolare", "Gondola": "Cabinovia", "Move panels back beside the rail": "Riporta i pannelli accanto alla barra", - "Move panels over the rail, to the edge": "Sposta i pannelli sopra la barra, fino al bordo" + "Move panels over the rail, to the edge": "Sposta i pannelli sopra la barra, fino al bordo", + "panel|VISUAL": "VISUALE", + "Look": "Aspetto", + "Overlay": "Overlay", + "Effects": "Effetti", + "Scope opacity": "Opacità dello scope", + "How dark the scope shades the view outside the keyhole": "Quanto lo scope scurisce la vista fuori dal foro", + "Reset to default": "Ripristina predefiniti", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Ripristina Visuale ai valori predefiniti: aspetto Normale, HUD, scope, etichette ed effetti" } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 3fb7ad023..24fa10f79 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -3534,5 +3534,13 @@ "Funicular": "ケーブルカー", "Gondola": "ゴンドラ", "Move panels back beside the rail": "パネルをレールの横に戻す", - "Move panels over the rail, to the edge": "パネルをレールの上、画面端へ移動" + "Move panels over the rail, to the edge": "パネルをレールの上、画面端へ移動", + "panel|VISUAL": "ビジュアル", + "Look": "見た目", + "Overlay": "オーバーレイ", + "Effects": "エフェクト", + "Scope opacity": "スコープの不透明度", + "How dark the scope shades the view outside the keyhole": "キーホール外のビューをスコープがどれだけ暗くするか", + "Reset to default": "既定に戻す", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "ビジュアルを既定に戻す:標準の見た目、HUD、スコープ、ラベル、エフェクト" } diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index a42bf0448..86b9f3aca 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -3708,5 +3708,13 @@ "Funicular": "Funicular", "Gondola": "Teleférico", "Move panels back beside the rail": "Voltar os painéis para junto da barra", - "Move panels over the rail, to the edge": "Mover os painéis sobre a barra, até a borda" + "Move panels over the rail, to the edge": "Mover os painéis sobre a barra, até a borda", + "panel|VISUAL": "VISUAL", + "Look": "Aparência", + "Overlay": "Sobreposição", + "Effects": "Efeitos", + "Scope opacity": "Opacidade da mira", + "How dark the scope shades the view outside the keyhole": "Quanto a mira escurece a vista fora do visor", + "Reset to default": "Redefinir para o padrão", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Redefinir o Visual para o padrão: aparência Normal, HUD, mira, rótulos e efeitos" } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 96efc6e5e..ac149c01f 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -3795,5 +3795,13 @@ "Funicular": "Фуникулёр", "Gondola": "Канатная дорога", "Move panels back beside the rail": "Вернуть панели к боковой панели", - "Move panels over the rail, to the edge": "Переместить панели поверх боковой панели к краю" + "Move panels over the rail, to the edge": "Переместить панели поверх боковой панели к краю", + "panel|VISUAL": "ВИЗУАЛ", + "Look": "Вид", + "Overlay": "Наложение", + "Effects": "Эффекты", + "Scope opacity": "Непрозрачность прицела", + "How dark the scope shades the view outside the keyhole": "Насколько прицел затемняет вид за пределами маски", + "Reset to default": "Сбросить по умолчанию", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Сбросить «Визуал» к настройкам по умолчанию: обычный вид, HUD, прицел, подписи и эффекты" } diff --git a/src/i18n/locales/sv.json b/src/i18n/locales/sv.json index 63304ba65..d6acc93a0 100644 --- a/src/i18n/locales/sv.json +++ b/src/i18n/locales/sv.json @@ -3621,5 +3621,13 @@ "Funicular": "Bergbana", "Gondola": "Kabinbana", "Move panels back beside the rail": "Flytta tillbaka panelerna bredvid listen", - "Move panels over the rail, to the edge": "Flytta panelerna över listen, till kanten" + "Move panels over the rail, to the edge": "Flytta panelerna över listen, till kanten", + "panel|VISUAL": "VISUELLT", + "Look": "Utseende", + "Overlay": "Överlägg", + "Effects": "Effekter", + "Scope opacity": "Scope-opacitet", + "How dark the scope shades the view outside the keyhole": "Hur mycket scopet mörkar vyn utanför nyckelhålet", + "Reset to default": "Återställ till standard", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Återställ Visuellt till standard: Normal stil, HUD, scope, etiketter och effekter" } diff --git a/src/i18n/locales/uk.json b/src/i18n/locales/uk.json index 47db81c3e..6f49092f4 100644 --- a/src/i18n/locales/uk.json +++ b/src/i18n/locales/uk.json @@ -3795,5 +3795,13 @@ "Funicular": "Фунікулер", "Gondola": "Канатна дорога", "Move panels back beside the rail": "Повернути панелі до бічної смуги", - "Move panels over the rail, to the edge": "Перемістити панелі поверх бічної смуги до краю" + "Move panels over the rail, to the edge": "Перемістити панелі поверх бічної смуги до краю", + "panel|VISUAL": "ВІЗУАЛ", + "Look": "Вигляд", + "Overlay": "Накладка", + "Effects": "Ефекти", + "Scope opacity": "Непрозорість прицілу", + "How dark the scope shades the view outside the keyhole": "Наскільки приціл затемнює вигляд за межами прицілу", + "Reset to default": "Скинути до типових", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Скинути «Візуал» до типових налаштувань: звичайний вигляд, HUD, приціл, підписи та ефекти" } diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 511b2f4ae..56a5aa354 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -3534,5 +3534,13 @@ "Funicular": "缆索铁路", "Gondola": "缆车", "Move panels back beside the rail": "将面板移回侧栏旁", - "Move panels over the rail, to the edge": "将面板移到侧栏上方,贴靠边缘" + "Move panels over the rail, to the edge": "将面板移到侧栏上方,贴靠边缘", + "panel|VISUAL": "视觉", + "Look": "外观", + "Overlay": "叠加层", + "Effects": "效果", + "Scope opacity": "视域不透明度", + "How dark the scope shades the view outside the keyhole": "视域遮罩使视域之外的画面变暗的程度", + "Reset to default": "恢复默认", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "将视觉恢复为默认设置:常规外观、HUD、视域、标签和效果" } diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index 385b18064..6f177a2cf 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -3534,5 +3534,13 @@ "Funicular": "纜索鐵路", "Gondola": "纜車", "Move panels back beside the rail": "將面板移回側欄旁", - "Move panels over the rail, to the edge": "將面板移到側欄上方,貼齊邊緣" + "Move panels over the rail, to the edge": "將面板移到側欄上方,貼齊邊緣", + "panel|VISUAL": "視覺", + "Look": "外觀", + "Overlay": "疊加層", + "Effects": "效果", + "Scope opacity": "視域不透明度", + "How dark the scope shades the view outside the keyhole": "視域遮罩使視域之外的畫面變暗的程度", + "Reset to default": "還原預設", + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "將視覺還原為預設設定:常規外觀、HUD、視域、標籤和效果" } diff --git a/src/i18n/same-as-english.json b/src/i18n/same-as-english.json index e95d5afc4..b8f50bfe7 100644 --- a/src/i18n/same-as-english.json +++ b/src/i18n/same-as-english.json @@ -113,6 +113,7 @@ "Layout", "Live", "London", + "Look", "Los Angeles", "MIL", "MINI", @@ -140,6 +141,7 @@ "Oldies", "OpenStreetMap + optional Google Maps Places", "Operator", + "Overlay", "PAUSE", "PLAY", "POS: LIVE", @@ -383,6 +385,7 @@ "mph", "nominal", "panel", + "panel|VISUAL", "view band|GLOBAL", "voice|ERROR", "{hours} h", @@ -732,6 +735,7 @@ "ON", "ON/OFF", "Oldies", + "Overlay", "PAYLOAD", "PLAY", "POS: LIVE", @@ -1047,6 +1051,7 @@ "mini", "mph", "nominal", + "panel|VISUAL", "via {source}", "view band|GLOBAL", "{hours} h", diff --git a/src/ui/PanelRailView.vue b/src/ui/PanelRailView.vue index 8ac9fb256..9f9712674 100644 --- a/src/ui/PanelRailView.vue +++ b/src/ui/PanelRailView.vue @@ -25,7 +25,9 @@ const items = computed(() => id: panel.id, icon: item?.icon ?? PANEL_RAIL_FALLBACK_ICON, // Static markup is translated at boot, so a host's title already is. - label: item ? t(item.label) : panel.title, + label: item + ? t(item.label, item.context && { context: item.context }) + : panel.title, }; }), ); diff --git a/src/ui/panelRailCatalog.js b/src/ui/panelRailCatalog.js index 6e0b0a876..854214a2a 100644 --- a/src/ui/panelRailCatalog.js +++ b/src/ui/panelRailCatalog.js @@ -9,7 +9,7 @@ export const PANEL_RAIL_FALLBACK_ICON = 'more_horiz'; /** * How each side panel appears on its rail. Labels are the panels' own titles. * `shownBy` names a class the panel needs to be shown at all (`f` toggles - * Data Layers' `active`). + * Data Layers' `active`). `context` is the label's translation context. */ export const PANEL_RAIL_ITEMS = Object.freeze({ 'data-panel': { @@ -19,7 +19,9 @@ export const PANEL_RAIL_ITEMS = Object.freeze({ }, 'utilities-panel': { icon: 'handyman', label: msg('UTILITIES') }, 'scene-panel': { icon: 'movie', label: msg('AUTHORING') }, - 'pp-toggles': { icon: 'palette', label: msg('VISUAL') }, + // `context` keeps the panel's name apart from the satellite class of the + // same English word. + 'pp-toggles': { icon: 'palette', label: msg('VISUAL'), context: 'panel' }, 'cctv-panel': { icon: 'videocam', label: msg('CCTV') }, 'weather-panel': { icon: 'partly_cloudy_day', label: msg('WEATHER') }, 'news-panel': { icon: 'public', label: msg('NEWS') }, diff --git a/src/ui/styles/visual-panel.css b/src/ui/styles/visual-panel.css index 696c08797..8a1339d45 100644 --- a/src/ui/styles/visual-panel.css +++ b/src/ui/styles/visual-panel.css @@ -202,7 +202,7 @@ checkbox, which already says it). */ #pp-toggles .pp-toggle-group > button.pp-toggle-btn::after, #pp-toggles .pp-panel-body > button.pp-toggle-btn::after { - content: 'OFF'; + content: var(--pp-pill-off, 'OFF'); margin-inline-start: auto; padding: 2px 8px; color: rgba(199, 214, 222, 0.5); @@ -216,7 +216,7 @@ #pp-toggles .pp-toggle-group > button.pp-toggle-btn.active::after, #pp-toggles .pp-toggle-group > button.pp-toggle-btn[aria-pressed='true']::after, #pp-toggles .pp-panel-body > button.pp-toggle-btn.active::after { - content: 'ON'; + content: var(--pp-pill-on, 'ON'); color: #7af0ff; border-color: rgba(54, 220, 255, 0.5); background: rgba(54, 220, 255, 0.1); diff --git a/src/ui/views/panels/DisplayPanel.vue b/src/ui/views/panels/DisplayPanel.vue index 695c3bc25..876982e86 100644 --- a/src/ui/views/panels/DisplayPanel.vue +++ b/src/ui/views/panels/DisplayPanel.vue @@ -23,6 +23,12 @@ const { view, models3d, parameters, cockpitPortal } = storeToRefs(display); // A control's change goes to its owner (DisplayBindings). const act = (name, ...args) => display.act(name, ...args); const pct = (value) => `${value}%`; +// The words of the on/off pill each card header draws (visual-panel.css), as +// CSS strings. +const pillWords = computed(() => ({ + '--pp-pill-on': JSON.stringify(t('ON')), + '--pp-pill-off': JSON.stringify(t('OFF')), +})); const hud = computed(() => view.value?.hud ?? null); const sonar = computed(() => view.value?.sonar ?? null); const sonarOn = computed(() => sonar.value?.enabled ?? true); @@ -54,12 +60,12 @@ const allocationOptions = computed(() => [