diff --git a/src/ui/visualEffects.test.mjs b/src/ui/visualEffects.test.mjs
index 9e9542c52..142bbbe4f 100644
--- a/src/ui/visualEffects.test.mjs
+++ b/src/ui/visualEffects.test.mjs
@@ -77,7 +77,7 @@ test('construction is inert and initialization creates one owned pipeline', () =
assert.equal(f.frames.size, 0);
f.effects.initStyles();
f.effects.initPostProcess();
- assert.equal(f.stages.size, 7);
+ assert.equal(f.stages.size, 6);
assert.ok(
Object.values(f.effects.stages).every(
(stage) => !stage.enabled && stage.uniforms.intensity === 0,
@@ -85,7 +85,7 @@ test('construction is inert and initialization creates one owned pipeline', () =
);
f.effects.initStyles();
f.effects.initPostProcess();
- assert.equal(f.stages.size, 7);
+ assert.equal(f.stages.size, 6);
f.effects.destroy();
});
@@ -252,7 +252,7 @@ test('stop revokes pending work immediately but retains stages until final destr
assert.equal(f.frames.size, 0);
assert.equal(
f.stages.size,
- 7,
+ 6,
'Context and Cockpit may still be releasing these stages',
);
const intensity = f.effects.stages.retro.uniforms.intensity;
@@ -281,7 +281,7 @@ test('destroying one instance does not remove another pipeline or clock', () =>
a.effects.destroy();
b.tick(250);
assert.equal(a.stages.size, 0);
- assert.equal(b.stages.size, 6);
+ assert.equal(b.stages.size, 5);
assert.equal(b.effects.stages.retro.uniforms.intensity, 0.5);
b.effects.destroy();
});
diff --git a/src/ui/visualInput.test.mjs b/src/ui/visualInput.test.mjs
index c69c935ba..a87c9c815 100644
--- a/src/ui/visualInput.test.mjs
+++ b/src/ui/visualInput.test.mjs
@@ -73,13 +73,13 @@ function shortcuts() {
return { documentRef, searchInput, calls, controller, press };
}
-test('number keys retain the seven style mappings', () => {
+test('number keys retain the six style mappings', () => {
const f = shortcuts();
for (const key of ['1', '2', '3', '4', '5', '6', '7', '8', 'Space'])
f.press(key);
assert.deepEqual(
f.calls,
- ['normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir', 'snow'].map(
+ ['normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir'].map(
(style) => ['setStyle', style],
),
);
diff --git a/src/ui/visualPresets.js b/src/ui/visualPresets.js
index e3830601a..ca251e48c 100644
--- a/src/ui/visualPresets.js
+++ b/src/ui/visualPresets.js
@@ -1,7 +1,6 @@
import { retroShader } from '../styles/retro.js';
import { animeShader } from '../styles/anime.js';
import { noirShader } from '../styles/noir.js';
-import { snowShader } from '../styles/snow.js';
import { nightVisionShader } from '../styles/surveillance.js';
import { thermalShader } from '../styles/thermal.js';
import { BLOOM_INTENSITY_DEFAULT } from '../bloom.js';
@@ -16,7 +15,6 @@ export const STYLES = {
thermal: thermalShader,
anime: animeShader,
noir: noirShader,
- snow: snowShader,
};
/**
@@ -156,5 +154,4 @@ export const STYLE_STATUS_LABELS = {
thermal: msg('FLIR'),
anime: msg('ANIME'),
noir: msg('NOIR'),
- snow: msg('SNOW'),
};
diff --git a/src/ui/visualSettings.js b/src/ui/visualSettings.js
index a6f7b5674..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 { 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';
@@ -1272,8 +1273,9 @@ export class VisualSettings {
const superseded = () => typeof isCurrent === 'function' && !isCurrent();
if (superseded()) return false;
- if (state.style && state.style !== this.activeStyle) {
- this.setStyle(state.style, { applyPreset: false });
+ const style = RETIRED_STYLES.includes(state.style) ? 'normal' : state.style;
+ if (style && style !== this.activeStyle) {
+ this.setStyle(style, { applyPreset: false });
}
const bloomState = state.bloom || {};
diff --git a/src/view/index.js b/src/view/index.js
index fc5385aaa..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({
@@ -20,7 +21,6 @@ export const STYLE_URL_NAMES = Object.freeze({
thermal: 'flir',
anime: 'anime',
noir: 'noir',
- snow: 'snow',
});
const STYLES_BY_URL_NAME = Object.freeze(
@@ -254,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))
@@ -370,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']);
diff --git a/src/voice/actionSchemas.js b/src/voice/actionSchemas.js
index bf7e2f66d..a27907666 100644
--- a/src/voice/actionSchemas.js
+++ b/src/voice/actionSchemas.js
@@ -248,15 +248,7 @@ const schemas = [
properties: {
style: {
type: 'string',
- enum: [
- 'normal',
- 'retro',
- 'surveillance',
- 'thermal',
- 'anime',
- 'noir',
- 'snow',
- ],
+ enum: ['normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir'],
},
},
required: ['style'],
diff --git a/src/voice/gevActions.js b/src/voice/gevActions.js
index 0a43f02f1..0fd17dc77 100644
--- a/src/voice/gevActions.js
+++ b/src/voice/gevActions.js
@@ -66,7 +66,6 @@ const ALLOWED_STYLES = new Set([
'thermal',
'anime',
'noir',
- 'snow',
]);
const PANEL_ALIASES = new Map([
['data', 'data-panel'],