From 3b6111be0672d86647e35674fded12211d3fdc87 Mon Sep 17 00:00:00 2001 From: Said Turksever <28983926+asturksever@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:34:19 +0000 Subject: [PATCH] Open views naming the retired Snow style on Normal createView threw on style 'snow', so an embed host or a show_in_gods_eye_view call holding a view from before Snow was retired was refused outright, camera and layers included. viewFromParams read style=snow as the recipient's own style, while the app opened the same link on Normal. Both now read a retired style as Normal; truly unknown names still throw. The retired list moves to a browser-free module shared by the view API, the share codec and visual settings. --- docs/CURRENT-STATE.md | 4 +++- docs/TOOLS.md | 3 ++- scripts/package-boundaries.json | 18 ++++++++++++------ src/app/embed.test.mjs | 14 ++++++++++++++ src/share/viewCodec.js | 2 +- src/ui/visualPresets.js | 6 ------ src/ui/visualSettings.js | 3 ++- src/view/index.js | 10 +++++++--- src/view/index.test.mjs | 8 ++++++++ src/visualStylePolicy.js | 7 +++++++ 10 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 src/visualStylePolicy.js diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 690024d28..4f3acfc1b 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1393,7 +1393,9 @@ no DOM dependency. Existing presets and the 500 ms transition remain unchanged. Stopping the controller revokes animation before asynchronous UI teardown; final destruction removes its stages and restores the borrowed bloom configuration. Snow is no longer a visual preset. A share link that names it, in either the -long or compact form, opens on Normal and drops the Snow shader parameters. +long or compact form, opens on Normal and drops the Snow shader parameters. A +view passed to embed mode or `show_in_gods_eye_view`, and a saved scene shot, +that names Snow also open on Normal. Sharpen runs only when enabled with intensity above 0.001. At slider zero its stage is disabled; raising the slider resumes it when sharpen is enabled. diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 45ee62546..80fd80b13 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -92,7 +92,8 @@ pitch), data layers, visual style, map imagery, and an aircraft, military aircraft or satellite to follow. `createView` builds and bounds one, `viewToParams` and `viewFromParams` write and read it in the share-link format the app restores, and `viewUrl` gives the address that opens it. The -style names are the ones share links use. Ships cannot be followed from a +style names are the ones share links use; a retired style (`snow`) is read +as `normal`. Ships cannot be followed from a link yet. An aircraft's `follow` may ask for its cockpit view (`cockpit: true`); links cannot carry that and open the app following it. diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 4df7229b4..9b46899d7 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -813,7 +813,8 @@ "src/ui/layerNames.js", "src/ui/statusText.js", "src/ui/statusBadge.js", - "src/scopeMaskPolicy.js" + "src/scopeMaskPolicy.js", + "src/visualStylePolicy.js" ], "external": ["cesium"] }, @@ -2627,7 +2628,8 @@ "src/voice/geminiCaptureWorklet.js", "src/ui/statusBadge.js", "src/sceneCommandRelease.js", - "src/scopeMaskPolicy.js" + "src/scopeMaskPolicy.js", + "src/visualStylePolicy.js" ], "external": [ "hls.js", @@ -3364,7 +3366,8 @@ "src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-Hant.json", "src/scopeMaskPolicy.js", - "src/ui/displayControls.js" + "src/ui/displayControls.js", + "src/visualStylePolicy.js" ], "external": ["cesium"] }, @@ -3941,7 +3944,8 @@ "src/data/idSwitches.js", "src/data/detectionPolicy.js", "src/scopeMaskPolicy.js", - "src/i18n/marker.js" + "src/i18n/marker.js", + "src/visualStylePolicy.js" ], "external": [] }, @@ -4045,7 +4049,8 @@ "src/tools/views.js", "src/tools/globePanel.js", "src/tools/queries/panelRequest.js", - "src/geoDistance.js" + "src/geoDistance.js", + "src/visualStylePolicy.js" ], "external": ["satellite.js"] }, @@ -4146,7 +4151,8 @@ "src/view/index.js", "src/data/idSwitches.js", "src/data/layerState.js", - "src/data/layerStateTokenReservations.json" + "src/data/layerStateTokenReservations.json", + "src/visualStylePolicy.js" ], "external": [] }, diff --git a/src/app/embed.test.mjs b/src/app/embed.test.mjs index 54799567e..5fa949342 100644 --- a/src/app/embed.test.mjs +++ b/src/app/embed.test.mjs @@ -146,6 +146,12 @@ test('an embedded page takes views only from its parent and answers it', async ( id: 3, view: { camera: { lat: 1, lon: 2 }, layers: ['flights'] }, }); + // A view kept from before Snow was retired still applies, on Normal. + send(parent, { + type: 'gev:view', + id: 4, + view: { camera: { lat: 1, lon: 2 }, style: 'snow' }, + }); await new Promise((resolve) => setTimeout(resolve, 10)); assert.deepEqual(posted[1], { type: 'gev:view-applied', @@ -161,6 +167,13 @@ test('an embedded page takes views only from its parent and answers it', async ( name === 'set_layer_visibility' && args.layerId === 'flights', ), ); + assert.equal(posted[3].id, 4); + assert.equal(posted[3].ok, true); + assert.ok( + calls.some( + ([name, args]) => name === 'set_visual_style' && args.style === 'normal', + ), + ); assert.equal(posted.filter((message) => message.id === 1).length, 0); // Ready names no data and goes to any parent; answers go only to the // origin that asked. @@ -168,6 +181,7 @@ test('an embedded page takes views only from its parent and answers it', async ( '*', 'https://host.example', 'https://host.example', + 'https://host.example', ]); remove(); }); diff --git a/src/share/viewCodec.js b/src/share/viewCodec.js index bb5f6d440..b72f79d34 100644 --- a/src/share/viewCodec.js +++ b/src/share/viewCodec.js @@ -31,9 +31,9 @@ import { } from '../data/detectionPolicy.js'; import { clampScopeFeatherPct } from '../scopeMaskPolicy.js'; import { VIEW_STYLES } from '../view/index.js'; +import { RETIRED_STYLES } from '../visualStylePolicy.js'; import { GLOBAL_POST_DEFAULTS, - RETIRED_STYLES, STYLE_PRESET_DEFAULTS, STYLES, } from '../ui/visualPresets.js'; diff --git a/src/ui/visualPresets.js b/src/ui/visualPresets.js index 90d89e2f0..ca251e48c 100644 --- a/src/ui/visualPresets.js +++ b/src/ui/visualPresets.js @@ -17,12 +17,6 @@ export const STYLES = { noir: noirShader, }; -/** - * Styles the app no longer offers. A share link, scene or shot that names one - * opens on Normal instead of keeping whatever style was already showing. - */ -export const RETIRED_STYLES = Object.freeze(['snow']); - /** * The tactical detection look: Dense at 75%. * diff --git a/src/ui/visualSettings.js b/src/ui/visualSettings.js index 11974307e..7c5a881eb 100644 --- a/src/ui/visualSettings.js +++ b/src/ui/visualSettings.js @@ -1,6 +1,7 @@ import { clampScopeFeatherPct } from '../scopeMaskPolicy.js'; import { syncScopeControls } from './displayControls.js'; -import { RETIRED_STYLES, STYLE_STATUS_LABELS } from './visualPresets.js'; +import { STYLE_STATUS_LABELS } from './visualPresets.js'; +import { RETIRED_STYLES } from '../visualStylePolicy.js'; import { syncSegmentedControl } from './segmentedControl.js'; import { t } from '../i18n/index.js'; import { UiLifetime } from './uiLifetime.js'; diff --git a/src/view/index.js b/src/view/index.js index 221c4e0a1..022914277 100644 --- a/src/view/index.js +++ b/src/view/index.js @@ -11,6 +11,7 @@ import { decodeLayerStateParams, encodeLayerStateParams, } from '../data/layerState.js'; +import { RETIRED_STYLES } from '../visualStylePolicy.js'; /** Visual styles by internal name, with the name share links use. */ export const STYLE_URL_NAMES = Object.freeze({ @@ -253,6 +254,7 @@ export function createView({ } = {}) { if (!Number.isFinite(camera?.lat) || !Number.isFinite(camera?.lon)) throw new TypeError('A view needs a camera lat and lon'); + if (RETIRED_STYLES.includes(style)) style = 'normal'; if (style !== null && !VIEW_STYLES.includes(style)) throw new TypeError(`Unknown view style: ${style}`); if (map !== null && !VIEW_MAPS.includes(map)) @@ -369,9 +371,11 @@ export function viewFromParams(params) { pitch_deg: number('pitch'), }, layers: state?.enabledLayerIds ?? [], - style: Object.hasOwn(STYLES_BY_URL_NAME, params.get('style')) - ? STYLES_BY_URL_NAME[params.get('style')] - : null, + style: RETIRED_STYLES.includes(params.get('style')) + ? 'normal' + : Object.hasOwn(STYLES_BY_URL_NAME, params.get('style')) + ? STYLES_BY_URL_NAME[params.get('style')] + : null, map: VIEW_MAPS.includes(map) ? map : null, follow, annotations: annotationsFromParams(params), diff --git a/src/view/index.test.mjs b/src/view/index.test.mjs index 34299c498..66630e6a0 100644 --- a/src/view/index.test.mjs +++ b/src/view/index.test.mjs @@ -127,6 +127,14 @@ test('annotations travel in the link, keeping only what the app draws', async () assert.equal(viewToParams(many).has('an'), false); }); +test('a view naming the retired Snow style opens on Normal', () => { + const camera = { lat: 1, lon: 2 }; + assert.equal(createView({ camera, style: 'snow' }).style, 'normal'); + const view = viewFromParams(new URLSearchParams('lat=1&lon=2&style=snow')); + assert.equal(view.style, 'normal'); + assert.equal(viewToParams(view).get('style'), 'normal'); +}); + test('inherited and unknown style keys do not corrupt a restored view', () => { for (const style of [ 'constructor', diff --git a/src/visualStylePolicy.js b/src/visualStylePolicy.js new file mode 100644 index 000000000..1a384a7a8 --- /dev/null +++ b/src/visualStylePolicy.js @@ -0,0 +1,7 @@ +/** + * Browser-free style vocabulary shared by views, share links and the UI. + * Styles the app no longer offers, by the name share links used: a view, + * link, scene or shot that names one opens on Normal instead of keeping + * whatever style was already showing. + */ +export const RETIRED_STYLES = Object.freeze(['snow']);