diff --git a/README.md b/README.md index 0a7509efa..745bec3d8 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Start with the included data sources, then add your own. Each layer is a separat - **🎯 Click-to-track anything:** Camera locks on, draws a fading trail, surfaces full metadata — and a tracked fire or vessel hands you off to the nearest live camera in one click. - **🖊️ Voice whiteboard:** Speak annotations onto the world — real boundary polygons, marks, and routes. - **🛫 3D hangar:** Real per-class aircraft models — 787, ATR-72, Citation, Bell 206, MQ-9 — and a tracked contact swaps from glyph to 3D model as you close in. -- **🎨 Reskin reality:** GLSL sensor looks over the normal globe — CRT, NVG, FLIR/thermal, Noir, Snow. +- **🎨 Reskin reality:** GLSL sensor looks over the normal globe — CRT, NVG, FLIR/thermal, Noir. - **🟩 Detection overlay:** Screen-space bounding boxes and IDs on everything in view. - **🎖️ Military HUD:** Tactical heads-up display with intelligence-style telemetry. - **🌐 Global Context:** Stage the full situational picture with one switch — and get your exact view back when you leave. @@ -294,14 +294,14 @@ Choose a first-run mission, or try these in order. The GIFs show Google Photorea ![Tracking the ISS along its orbital path as it crosses over Ukraine](docs/media/14-iss-over-ukraine.gif) -6. **Switch the optics.** Tap `1`–`7` — CRT, NVG, FLIR — and the whole live planet re-renders through a different sensor. +6. **Switch the optics.** Tap `1`–`6` — CRT, NVG, FLIR — and the whole live planet re-renders through a different sensor. ![Cycling a dense live globe through CRT, FLIR, and NVG in one continuous view](docs/media/01-style-sweep.gif) 7. **Talk to it** _(needs an OpenAI key)_: _"Take me to LAX and select the nearest airborne aircraft."_ 8. **Come home.** Hit **Reset Globe** — or just say _"zoom out to a globe view."_ -**Keyboard:** `1`–`7` visual styles · `H` HUD · `D` detection · `C` cockpit · `` ` `` frame rate · `Esc` out. Trackpad pinch zooms the globe. +**Keyboard:** `1`–`6` visual styles · `H` HUD · `D` detection · `C` cockpit · `` ` `` frame rate · `Esc` out. Trackpad pinch zooms the globe. --- diff --git a/changelog.d/1468-remove-snow-preset.md b/changelog.d/1468-remove-snow-preset.md new file mode 100644 index 000000000..2bd1cb161 --- /dev/null +++ b/changelog.d/1468-remove-snow-preset.md @@ -0,0 +1,4 @@ +- Remove the Snow visual preset. Visual Presets, keyboard shortcuts, the + Cockpit vision carousel and the visual-style voice action now offer Normal, + CRT, NVG, FLIR, Anime and Noir; share links and scene shots that name Snow + open on Normal (asturksever, #1468). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 50b1b863a..66a6c4fa5 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1392,8 +1392,10 @@ and sharpen. Display supplies actions and renders settings; effect execution has 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's sideways sway stays within a fixed range at any session age; Wind scales -its amplitude. +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. 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. @@ -2831,7 +2833,7 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with > failed searches leave the current camera owner untouched, and Context Focus > can return to the preserved Contact. Focus also restores the selected > aircraft's canonical follow frame after a manual zoom-away. -> Visual presets retain the order Normal, CRT, NVG, FLIR, Anime, Noir, Snow. +> Visual presets retain the order Normal, CRT, NVG, FLIR, Anime, Noir. > Configurable preset selection—including same-style reselection—opens the > shared Parameters surface directly below Detection and scrolls it into > view; share-link restoration @@ -2883,7 +2885,7 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with > Inside Cockpit, the focused summary card is titled Contact in both visible > copy and its accessible control labels. > The top-center Cockpit vision cycle is the fixed, duplicate-free sequence -> Normal, CRT, NVG, FLIR, Anime, Noir, and Snow. Entry selects the map preset +> Normal, CRT, NVG, FLIR, Anime, and Noir. Entry selects the map preset > already in use, so a FLIR map opens on the existing FLIR item while > Normal remains available exactly once. NONE is not offered. Cockpit choices > temporarily override the map preset, while Exit Cockpit and Reset both restore @@ -5172,8 +5174,8 @@ and unreachable upstream (502/504) separately from road geometry. first, activates the Contacts sub-view, and returns the settled 250 km window. Its `aircraft` count is the exact civilian-plus-military total when both feeds can answer, or `unknown` when either component is unavailable. -- Cockpit's top vision switch cycles all seven map looks: Normal, CRT, NVG, - FLIR, Anime, Noir, and Snow. Entry selects the matching map style without adding +- Cockpit's top vision switch cycles all six map looks: Normal, CRT, NVG, + FLIR, Anime, and Noir. Entry selects the matching map style without adding a duplicate carousel item. There is no empty `NONE` entry. ### Live AIS Vessels (June 2026) @@ -5659,7 +5661,7 @@ apply to `public/` assets or built-preview serving. - **Cockpit left-panel clearance:** the Cockpit Contact card and peripheral HUD participate in the adaptive left accordion's live obstacle measurements, including live viewport-height changes. Expanding Layers or Scenes keeps the active panel in the available upper-left corridor with internal scrolling; it does not cover the Contact card, lower Cockpit controls, or Cesium credit line. Outside Cockpit the hidden card does not alter the normal corridor. - **Cockpit Context scope:** the 250 km radius applies to the air/sea proximity cohorts. Installation counts come only from the currently loaded viewport and are labeled `CURRENT VIEWPORT ONLY` in the cockpit as well as the normal Context panel; neither surface presents them as a complete 250 km installation survey. - **Cockpit camera anchor:** first-person mode does not write feed-boundary corrections directly into the camera. A cockpit-only inertial anchor advances from the selected aircraft's displayed course and speed, then converges toward the authoritative delayed track with correction capped below forward motion. The displayed kinematics are derived from the same consecutive fix segment as the rendered position, with raw feed speed/course used only as fallback; a transient zero/missing feed speed therefore cannot freeze a visibly moving aircraft after layer enable or a map/cockpit handoff. Rendered altitude continues to come from that interpolated track position. Late ADS-B fixes and short render stalls can remove drift without accelerating or reversing the view. Camera placement runs before scene update/culling at a bounded 20 Hz so a moving cockpit does not force Photoreal 3D Tiles to retraverse on every display frame; textual instruments update at 10 Hz and context/layout work at 4 Hz. Every far Cockpit contact pip shares one stable Cesium texture-atlas entry and skips unused screen-projected course calculations, while only in-range 2D aircraft silhouettes pay the screen-projected rotation cost; ambient glTF collections are hidden/retained rather than synchronously destroyed at cockpit entry, and context rails lay out only on explicit content/state changes and viewport resize. The deliberate 15/30-second layer interpolation delays and per-Cesium-frame position caches remain unchanged. -- **Cockpit route, vision, and view controls:** visible on-screen `COCKPIT`, `RESET`, and `EXIT COCKPIT` controls replace reliance on the `C` shortcut. RESET uses the same canonical globe route as the map and voice actions, exits Cockpit, and releases its camera ownership rather than exposing the hidden map-style top action. When the tracked commercial flight has a plausible ADSBDB route, the top of the right briefing rail shows a compact `FROM → TO` airport strip and the visor shows a centered estimated-destination chevron with its relative bearing; its destination label rounds to whole degrees in 000–359, wrapping rounded north to 000° while the chevron continues to use the relative bearing independently of label rounding; absent or implausible route data hides the strip and cue rather than guessing. The cockpit-local vision control is an interactive `PREV / CURRENT / NEXT` carousel over `NORMAL`, `CRT`, `NVG`, `FLIR`, `ANIME`, `NOIR`, and `SNOW`; its previous/next actions wrap, and activating the current value advances to the next style. Cockpit enters on the active map preset, so a Cyber FLIR map opens Cockpit on FLIR while Normal remains a distinct option instead of producing a second FLIR entry. Cockpit choices temporarily activate the existing Cesium post-process stages, while exiting Cockpit through either EXIT or RESET restores the pre-entry map style. The regional-news page uses a free Google News RSS locality query first, with the existing GDELT query retained only as a fail-soft fallback; linked headlines remain reporting, not verified incidents or risk intelligence. +- **Cockpit route, vision, and view controls:** visible on-screen `COCKPIT`, `RESET`, and `EXIT COCKPIT` controls replace reliance on the `C` shortcut. RESET uses the same canonical globe route as the map and voice actions, exits Cockpit, and releases its camera ownership rather than exposing the hidden map-style top action. When the tracked commercial flight has a plausible ADSBDB route, the top of the right briefing rail shows a compact `FROM → TO` airport strip and the visor shows a centered estimated-destination chevron with its relative bearing; its destination label rounds to whole degrees in 000–359, wrapping rounded north to 000° while the chevron continues to use the relative bearing independently of label rounding; absent or implausible route data hides the strip and cue rather than guessing. The cockpit-local vision control is an interactive `PREV / CURRENT / NEXT` carousel over `NORMAL`, `CRT`, `NVG`, `FLIR`, `ANIME`, and `NOIR`; its previous/next actions wrap, and activating the current value advances to the next style. Cockpit enters on the active map preset, so a Cyber FLIR map opens Cockpit on FLIR while Normal remains a distinct option instead of producing a second FLIR entry. Cockpit choices temporarily activate the existing Cesium post-process stages, while exiting Cockpit through either EXIT or RESET restores the pre-entry map style. The regional-news page uses a free Google News RSS locality query first, with the existing GDELT query retained only as a fail-soft fallback; linked headlines remain reporting, not verified incidents or risk intelligence. - Cockpit wind-direction labels wrap after rounding to whole degrees, so a direction of 359.5° displays as `N · 0°` rather than `N · 360°`. - **Cockpit missing observations:** missing local weather observations remain unknown rather than being converted to zero or clear skies, in the briefing and in the cloud effects; a missing temperature drops the whole local weather record, including its wind and condition. - **Cockpit weather status:** the earlier multi-canvas atmospheric compositor remains fail-closed and is not attached to the live viewer. Cockpit clouds are a separate transparent WebGL pass with a capped 520×320 framebuffer, 24 ray steps, three FBM octaves, and a 12 FPS ceiling. It defaults off and starts only when local storage explicitly contains the persisted `WX ON` opt-in (`'1'`). When opted in, observations refresh after five minutes or 25 km of aircraft movement, fail transparent when unavailable or clear, and stop on exit or disable. `WX OFF` governs atmospheric rendering only: the briefing still fetches source-backed Natural Earth region, headline, and Open-Meteo local-information data, aborting and replacing any in-flight request when the selected aircraft changes. No weather effect runs in map mode and no synthetic fallback is shown. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 65d0abae9..7bf6d3751 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -97,7 +97,8 @@ operational sample contained 3,909 observations and two selected labels, but its live aircraft and traffic rows were empty, so it remains a limited stress case. Snow, Noir, dense detection, and text-heavy combined layers are the clearest -controlled comparison points for later optimization work. +controlled comparison points for later optimization work. Snow has since been +retired as a visual preset; its row records the measurement as taken. ## Keyed live sources diff --git a/docs/SHARE-LINK-FORMAT.md b/docs/SHARE-LINK-FORMAT.md index cfb6d50fb..ea5636f39 100644 --- a/docs/SHARE-LINK-FORMAT.md +++ b/docs/SHARE-LINK-FORMAT.md @@ -90,35 +90,35 @@ probability of about 255 in 256 (many of them already fail as base64url). ## Field registry (top level) -| # | State key | Type | Default | Encoding | -| --- | ---------------------------- | ----- | ----------- | ------------------------------------------------------- | -| 1 | `view.kind` | enum | see below | 0 place, 1 thing | -| 2 | `lat` | coord | 0 | zigzag of degrees × 10⁴ | -| 3 | `lon` | coord | 0 | zigzag of degrees × 10⁴ | -| 4 | `alt` | sint | 800 | whole metres | -| 5 | `heading` | angle | 0 | whole degrees, see angles | -| 6 | `pitch` | sint | −35 | whole degrees | -| 7 | `roll` | angle | 0 | whole degrees | -| 8 | `sharedAtMs` | epoch | none | zigzag seconds since 2026-01-01T00:00:00Z | -| 9 | booleans | flags | 0 | bit set when the value differs from default | -| 10 | `layerState.enabledLayerIds` | LEN | none | layer bitset, see layers | -| 11 | `layerState.options` | LEN | none | packed option triples, see layers | -| 12 | `panelState` | LEN | none | packed panel entries, see panels | -| 13 | thing | LEN | none | sub-message, see thing views | -| 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow | -| 15 | `mapStack` | enum | `photoreal` | photoreal, bing-aerial, bing-labels, esri-imagery, osm | -| 16 | `hudVariant` | enum | style | tactical, operator, minimal, cyber | -| 17 | `bloomIntensity` | uint | style | | -| 18 | `bloomVersion` | uint | 2 | | -| 19 | `sharpenIntensity` | uint | style | | -| 20 | `detectionMode` | enum | style | OFF, SPARSE, BALANCED, DENSE | -| 21 | `detectionDensity` | uint | style | one of 0, 25, 50, 75, 100 | -| 22 | `detectionAllocation` | enum | style | ELASTIC, WEIGHTED | -| 23 | `detectionFadePct` | uint | style | 0–40 | -| 24 | `detectionOutsideOpacityPct` | uint | style | 0–100 | -| 25 | `scopeFeatherPct` | uint | 100 | 0–200 | -| 26 | `scopeTerminusPct` | uint | none | 94–100; absent is the adaptive terminus | -| 27 | `styleParams` | LEN | style | packed `(token, value × 100)` pairs | +| # | State key | Type | Default | Encoding | +| --- | ---------------------------- | ----- | ----------- | ----------------------------------------------------------------- | +| 1 | `view.kind` | enum | see below | 0 place, 1 thing | +| 2 | `lat` | coord | 0 | zigzag of degrees × 10⁴ | +| 3 | `lon` | coord | 0 | zigzag of degrees × 10⁴ | +| 4 | `alt` | sint | 800 | whole metres | +| 5 | `heading` | angle | 0 | whole degrees, see angles | +| 6 | `pitch` | sint | −35 | whole degrees | +| 7 | `roll` | angle | 0 | whole degrees | +| 8 | `sharedAtMs` | epoch | none | zigzag seconds since 2026-01-01T00:00:00Z | +| 9 | booleans | flags | 0 | bit set when the value differs from default | +| 10 | `layerState.enabledLayerIds` | LEN | none | layer bitset, see layers | +| 11 | `layerState.options` | LEN | none | packed option triples, see layers | +| 12 | `panelState` | LEN | none | packed panel entries, see panels | +| 13 | thing | LEN | none | sub-message, see thing views | +| 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow (retired) | +| 15 | `mapStack` | enum | `photoreal` | photoreal, bing-aerial, bing-labels, esri-imagery, osm | +| 16 | `hudVariant` | enum | style | tactical, operator, minimal, cyber | +| 17 | `bloomIntensity` | uint | style | | +| 18 | `bloomVersion` | uint | 2 | | +| 19 | `sharpenIntensity` | uint | style | | +| 20 | `detectionMode` | enum | style | OFF, SPARSE, BALANCED, DENSE | +| 21 | `detectionDensity` | uint | style | one of 0, 25, 50, 75, 100 | +| 22 | `detectionAllocation` | enum | style | ELASTIC, WEIGHTED | +| 23 | `detectionFadePct` | uint | style | 0–40 | +| 24 | `detectionOutsideOpacityPct` | uint | style | 0–100 | +| 25 | `scopeFeatherPct` | uint | 100 | 0–200 | +| 26 | `scopeTerminusPct` | uint | none | 94–100; absent is the adaptive terminus | +| 27 | `styleParams` | LEN | style | packed `(token, value × 100)` pairs | Field 1 absent means `thing` when field 13 is present and `place` otherwise, so v1 never writes it; a later view kind writes its code. @@ -163,7 +163,7 @@ for it.) In v1: | thermal | sensitivity 0.85, bloom 0.2, mode 0.33, pixelation 1, palette 0 | | anime | saturation 1, edgeThick 0.5 | | noir | contrastAmt 1.2, grainAmt 0.5, vignetteAmt 0.5 | -| snow | density 0.6, wind 0.5 | +| snow | retired: decodes as normal; its parameters are dropped | These are frozen like every other default: the codec reads them from the app's tables today, and a test pins them, so if a table moves the old values @@ -342,7 +342,9 @@ the app that can open it. values in the codec (see Style defaults). The same applies to `SHARE_VIEW_LAYER_OPTION_ABSENT` and the panel defaults. 4. **Codes are append-only.** Enum lists, entity kinds, flag bits, panel - numbers and style parameter tokens only grow. + numbers and style parameter tokens only grow. A style the app no longer + offers keeps its code: `snow` (6) is retired, so a link that names it + opens as Normal and the parameters written for it are dropped. 5. **Add, don't change.** A new setting takes the next free field number (28 onwards; fields 1–15 have one-byte tags and are all taken). A new boolean takes the next flag bit instead, which costs nothing when it is at 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/format-scope.json b/scripts/format-scope.json index 5947f8811..228bf6a98 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -212,7 +212,6 @@ "src/styles/thermal.js", "src/styles/anime.js", "src/styles/noir.js", - "src/styles/snow.js", "scripts/qa-visual-effects.mjs", "scripts/qa-draw-tool.mjs", "scripts/qa-geo-import.mjs", diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 4e70afc94..b49872430 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -466,7 +466,6 @@ "src/styles/thermal.js", "src/styles/anime.js", "src/styles/noir.js", - "src/styles/snow.js", "src/i18n/marker.js" ], "external": ["cesium"] @@ -724,7 +723,6 @@ "src/styles/anime.js", "src/styles/noir.js", "src/styles/retro.js", - "src/styles/snow.js", "src/styles/surveillance.js", "src/styles/thermal.js", "src/ui/applicationShell.js", @@ -845,7 +843,8 @@ "src/ui/layerNames.js", "src/ui/statusText.js", "src/ui/statusBadge.js", - "src/scopeMaskPolicy.js" + "src/scopeMaskPolicy.js", + "src/visualStylePolicy.js" ], "external": ["cesium"] }, @@ -965,7 +964,8 @@ "src/i18n/locales/uk.json", "src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-Hant.json", - "src/ui/unitLabels.js" + "src/ui/unitLabels.js", + "src/visualStylePolicy.js" ], "external": ["cesium"] }, @@ -2327,7 +2327,6 @@ "src/styles/anime.js", "src/styles/noir.js", "src/styles/retro.js", - "src/styles/snow.js", "src/styles/surveillance.js", "src/styles/thermal.js", "src/ui/applicationShell.js", @@ -2720,7 +2719,8 @@ "src/voice/geminiCaptureWorklet.js", "src/ui/statusBadge.js", "src/sceneCommandRelease.js", - "src/scopeMaskPolicy.js" + "src/scopeMaskPolicy.js", + "src/visualStylePolicy.js" ], "external": [ "hls.js", @@ -3446,7 +3446,6 @@ "src/styles/anime.js", "src/styles/noir.js", "src/styles/retro.js", - "src/styles/snow.js", "src/styles/surveillance.js", "src/styles/thermal.js", "src/ui/applicationShortcuts.js", @@ -3470,7 +3469,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"] }, @@ -4060,7 +4060,6 @@ "src/styles/anime.js", "src/styles/noir.js", "src/styles/retro.js", - "src/styles/snow.js", "src/styles/surveillance.js", "src/styles/thermal.js", "src/view/index.js", @@ -4069,7 +4068,8 @@ "src/data/idSwitches.js", "src/data/detectionPolicy.js", "src/scopeMaskPolicy.js", - "src/i18n/marker.js" + "src/i18n/marker.js", + "src/visualStylePolicy.js" ], "external": [] }, @@ -4176,7 +4176,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"] }, @@ -4277,7 +4278,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/scripts/qa-cockpit-utility.mjs b/scripts/qa-cockpit-utility.mjs index cb3515ed2..2bfc1ca41 100644 --- a/scripts/qa-cockpit-utility.mjs +++ b/scripts/qa-cockpit-utility.mjs @@ -1603,7 +1603,7 @@ try { cockpit.setVisionMode('optical'); current.focus({ preventScroll: true }); const states = [read()]; - for (let index = 0; index < 7; index += 1) { + for (let index = 0; index < 6; index += 1) { current.click(); states.push(read()); } @@ -1637,36 +1637,36 @@ try { reentered, }; }); - const cycleModes = ['optical', 'crt', 'nvg', 'thermal', 'anime', 'noir', 'snow', 'optical']; - const cycleLabels = ['NORMAL', 'CRT', 'NVG', 'FLIR', 'ANIME', 'NOIR', 'SNOW', 'NORMAL']; + const cycleModes = ['optical', 'crt', 'nvg', 'thermal', 'anime', 'noir', 'optical']; + const cycleLabels = ['NORMAL', 'CRT', 'NVG', 'FLIR', 'ANIME', 'NOIR', 'NORMAL']; check( - 'Cockpit vision control exposes Normal, CRT, NVG, FLIR, Anime, Noir and Snow before wrapping', + 'Cockpit vision control exposes Normal, CRT, NVG, FLIR, Anime and Noir before wrapping', JSON.stringify(visionCycle.states.map((state) => state.mode)) === JSON.stringify(cycleModes) && JSON.stringify(visionCycle.states.map((state) => state.label)) === JSON.stringify(cycleLabels) && visionCycle.states[5]?.aria === 'Current cockpit vision style: Noir. Activate for next style.' - && visionCycle.states[7]?.aria === 'Current cockpit vision style: Normal. Activate for next style.', + && visionCycle.states[6]?.aria === 'Current cockpit vision style: Normal. Activate for next style.', JSON.stringify(visionCycle.states.map(({ mode, label, aria }) => ({ mode, label, aria }))), ); check( 'user vision changes open Display for every temporary treatment and retain selector focus', - visionCycle.states.slice(1, 7).every((state) => state.displayOpen === 'true') - && visionCycle.states.slice(1, 7).every((state) => state.radioOpen === 'false') - && visionCycle.states.slice(1, 7).every((state) => state.parametersActive) - && visionCycle.states.slice(1, 7).every((state) => !state.parametersCollapsed) - && visionCycle.states.slice(1, 7).every((state) => state.signalCollapsed) + visionCycle.states.slice(1, 6).every((state) => state.displayOpen === 'true') + && visionCycle.states.slice(1, 6).every((state) => state.radioOpen === 'false') + && visionCycle.states.slice(1, 6).every((state) => state.parametersActive) + && visionCycle.states.slice(1, 6).every((state) => !state.parametersCollapsed) + && visionCycle.states.slice(1, 6).every((state) => state.signalCollapsed) && visionCycle.states.every((state) => state.focusRetained), JSON.stringify(visionCycle.states), ); check( 'each Cockpit vision mode drives only its own shader and Normal clears them all', - visionCycle.states.slice(1, 7).every((state) => { + visionCycle.states.slice(1, 6).every((state) => { const target = { - crt: 'retro', nvg: 'surveillance', thermal: 'thermal', anime: 'anime', noir: 'noir', snow: 'snow', + crt: 'retro', nvg: 'surveillance', thermal: 'thermal', anime: 'anime', noir: 'noir', }[state.mode]; return Object.entries(state.intensities).every(([name, value]) => name === target ? value > 0 : value === 0); }) - && Object.values(visionCycle.states[7].intensities).every((value) => value === 0), + && Object.values(visionCycle.states[6].intensities).every((value) => value === 0), JSON.stringify(visionCycle.states.map(({ mode, intensities }) => ({ mode, intensities }))), ); check( @@ -2317,13 +2317,13 @@ try { const styleManager = window.__godsEyeView.styleManager; const styleBefore = styleManager.activeStyle; // A Cockpit-only choice that Reset must undo. - styleManager.cockpitView.setVisionMode('snow'); + styleManager.cockpitView.setVisionMode('anime'); window.__qaCockpitReset = { calls: 0, original: manager.resetToGlobeView, subjectId: awareness?.getContextSnapshot?.()?.subject?.id || null, styleBefore, - snowDuringCockpit: styleManager.stages?.snow?.uniforms?.intensity, + animeDuringCockpit: styleManager.stages?.anime?.uniforms?.intensity, }; manager.resetToGlobeView = function qaCountedCockpitReset(...args) { window.__qaCockpitReset.calls += 1; @@ -2360,7 +2360,7 @@ try { height, subjectPreserved: Boolean(qa.subjectId && subjectId === qa.subjectId), styleRestored: gev.styleManager.activeStyle === qa.styleBefore, - snowDuringCockpit: qa.snowDuringCockpit, + animeDuringCockpit: qa.animeDuringCockpit, // Cockpit-time intensities are Cockpit's own, so check against the map preset. visionRestored: Object.entries(gev.styleManager.stages || {}).every(([name, stage]) => name === gev.styleManager.activeStyle @@ -2376,7 +2376,7 @@ try { && resetState.resetHidden && Math.abs(resetState.height - 18_000_000) < 150_000 && resetState.subjectPreserved - && resetState.snowDuringCockpit > 0 + && resetState.animeDuringCockpit > 0 && resetState.styleRestored && resetState.visionRestored, JSON.stringify(resetState), diff --git a/scripts/qa-map-source-tray.mjs b/scripts/qa-map-source-tray.mjs index 2876e0cac..8a0c0b7fd 100644 --- a/scripts/qa-map-source-tray.mjs +++ b/scripts/qa-map-source-tray.mjs @@ -200,8 +200,8 @@ const checkResponsiveStyleFocus = async (width) => { await page.focus('#control-panel-toggle'); await pressTabs(2); // disclosure -> pin -> Normal } - for (const [index, style] of ['normal', 'thermal', 'snow'].entries()) { - if (index) await pressTabs(3); + for (const [tabs, style] of [[0, 'normal'], [3, 'thermal'], [2, 'noir']]) { + if (tabs) await pressTabs(tabs); const state = await styleFocusMetrics(); check(`${width} px ${style} keyboard ring is visible and unclipped`, hasVisibleStyleFocus(state, style) && state.selectedStyle === 'normal' @@ -567,13 +567,13 @@ try { // Visual Styles precede the five Map Source tiles. Keep the existing opening // handoff to selected OSM, then use the user's real Shift+Tab path into styles. await pressTabs(5, true); - const snowFocus = await styleFocusMetrics(); - check('Shift+Tab reaches Snow with a visible ring while Normal and OSM stay selected', - hasVisibleStyleFocus(snowFocus, 'snow') && !snowFocus.selected - && snowFocus.selectedStyle === 'normal' && snowFocus.selectedMap === 'osm', - JSON.stringify(snowFocus)); + const noirFocus = await styleFocusMetrics(); + check('Shift+Tab reaches Noir with a visible ring while Normal and OSM stay selected', + hasVisibleStyleFocus(noirFocus, 'noir') && !noirFocus.selected + && noirFocus.selectedStyle === 'normal' && noirFocus.selectedMap === 'osm', + JSON.stringify(noirFocus)); await page.screenshot({ path: path.join(shotsDir, 'keyboard-style-focus.png') }); - await pressTabs(3, true); + await pressTabs(2, true); const middleFocus = await styleFocusMetrics(); check('Shift+Tab traverses the middle FLIR style without activating it', hasVisibleStyleFocus(middleFocus, 'thermal') && !middleFocus.selected diff --git a/scripts/qa-visual-effects.mjs b/scripts/qa-visual-effects.mjs index db0696e08..f3b9ab8bd 100644 --- a/scripts/qa-visual-effects.mjs +++ b/scripts/qa-visual-effects.mjs @@ -80,7 +80,6 @@ try { 'thermal', 'anime', 'noir', - 'snow', 'normal', ]) { await page.evaluate( diff --git a/server/providers/openai/instructions.js b/server/providers/openai/instructions.js index c9c4a43b4..2841df78a 100644 --- a/server/providers/openai/instructions.js +++ b/server/providers/openai/instructions.js @@ -46,7 +46,7 @@ function realtimeInstructions( // string is the whole rollback. 'NAMED VIEWS are shorthand for tool calls you already have — there is no "mode" tool for them. Treat ONLY these as the shorthand: "infrastructure mode" / "the infrastructure view" / "show me global infrastructure" means three set_layer_visibility calls (local-datacenters, local-dams, telegeography-submarine-cables) plus zoom_to_globe; "environmental mode" / "earth watch" / "active events", said as the name of a view, means set_layer_visibility for local-firms and earthquakes plus zoom_to_globe. Anything vaguer is NOT this shorthand — an open-ended question about the world or the news is an ordinary question: answer it, or use analyst_query over the layers already on. Never switch a whole view on to answer a question nobody asked to see. When you do run one, make every call before speaking, then give one confirmation naming the resulting state; if the fires layer comes back unavailable because no FIRMS key is configured, say so plainly — the earthquakes still loaded. "Live contacts" and "space missions" are NOT this pattern: they stay set_context_mode{mode:"contacts"} and set_context_mode{mode:"space-missions"}.', 'For visual filter requests, call set_visual_style with one of the allowed style IDs.', - 'SHIPS vs STYLES: "vessels" is easily heard as "visuals". "Show me vessels", "turn on vessels", "the vessels", "visuals" or "vestibules" said WITHOUT a style name means the ships layer: set_layer_visibility{layerId:"ais-live-vessels"}, exactly as "ships" or "boats" does. Call set_visual_style only when the user names a style (retro/CRT, surveillance/night vision, thermal, anime, noir, snow, normal) or says filter, effect or look. Never turn on a different data layer (such as fires) for a ships request.', + 'SHIPS vs STYLES: "vessels" is easily heard as "visuals". "Show me vessels", "turn on vessels", "the vessels", "visuals" or "vestibules" said WITHOUT a style name means the ships layer: set_layer_visibility{layerId:"ais-live-vessels"}, exactly as "ships" or "boats" does. Call set_visual_style only when the user names a style (retro/CRT, surveillance/night vision, thermal, anime, noir, normal) or says filter, effect or look. Never turn on a different data layer (such as fires) for a ships request.', 'Disambiguation table — basemap vs layer vs style: basemap switching requires an explicit stack name — "Bing aerial" means set_map_stack bing-aerial, "aerial with labels" means bing-labels, "OSM"/"road map" means osm, "Esri"/"Esri imagery" means esri-imagery, "Google 3D"/"photorealistic" means photoreal. Any other mention of "satellite" or "satellites" means the satellites DATA LAYER via set_layer_visibility, never a basemap — except "recent/latest satellite imagery" (the recent-imagery layer) and "weather satellite"/clouds (weather-satellite). "surveillance"/"night vision"/"thermal" are visual STYLES via set_visual_style.', 'HUD requests ("hud on/off", "switch to operator/minimal/tactical/cyber layout") use set_hud. Detection requests ("detection on", "dense mode", "balanced mode", "sparse mode", "set density to 25", "use weighted allocation") use set_detection. Density snaps to 0/25/50/75/100 and derives Sparse/Balanced/Dense; panoptic is a legacy alias for Dense.', 'Cyber sonar requests use set_cyber_sonar: enabled, rings, rangePct, intensityPct (Power), opacityPct, sectorDeg. For settings questions read get_current_view_state.controls.hud.sonar. Sonar is not detection. Do not silently switch themes, reveal the HUD or enable sonar when changing a slider. If the user explicitly requests Cyber plus sonar settings, call set_hud first, wait for success, then set_cyber_sonar. Confirm returned values; mapSweepActive and contactSweepActive distinguish saved settings from active map effects, including in Cockpit or reduced motion. Do not claim visible contacts solely because enabled is true.', 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/celestialRing.test.mjs b/src/celestialRing.test.mjs index 7260c99fc..6ee9648b3 100644 --- a/src/celestialRing.test.mjs +++ b/src/celestialRing.test.mjs @@ -87,7 +87,7 @@ test('circular angle distance remains small across the wrap point', () => { test('celestial ring is available only in Normal style', () => { assert.equal(isCelestialRingStyleSupported('normal'), true); - for (const style of ['retro', 'surveillance', 'thermal', 'anime', 'noir', 'snow']) { + for (const style of ['retro', 'surveillance', 'thermal', 'anime', 'noir']) { assert.equal(isCelestialRingStyleSupported(style), false); } }); diff --git a/src/cockpitMarkup.test.mjs b/src/cockpitMarkup.test.mjs index 0b376d9fc..c47b29c5c 100644 --- a/src/cockpitMarkup.test.mjs +++ b/src/cockpitMarkup.test.mjs @@ -151,7 +151,7 @@ test('Cockpit vision cycle exposes every map preset exactly once', () => { ); assert.match( setVisionMode.toString(), - /const labels = \{\s*optical: t\('NORMAL'\),\s*crt: t\('CRT'\),\s*nvg: t\('NVG'\),\s*thermal: t\('FLIR'\),\s*anime: t\('ANIME'\),\s*noir: t\('NOIR'\),\s*snow: t\('SNOW'\),?\s*\};/, + /const labels = \{\s*optical: t\('NORMAL'\),\s*crt: t\('CRT'\),\s*nvg: t\('NVG'\),\s*thermal: t\('FLIR'\),\s*anime: t\('ANIME'\),\s*noir: t\('NOIR'\),?\s*\};/, ); assert.doesNotMatch(setVisionMode.toString(), /none: 'NONE'/); assert.doesNotMatch(ui, /getInheritedVisionLabel/); diff --git a/src/cockpitVisionPolicy.js b/src/cockpitVisionPolicy.js index 1b1c4e4b8..a6c319269 100644 --- a/src/cockpitVisionPolicy.js +++ b/src/cockpitVisionPolicy.js @@ -5,7 +5,6 @@ export const COCKPIT_VISION_MODES = Object.freeze([ 'thermal', 'anime', 'noir', - 'snow', ]); const TARGET_STYLE_BY_MODE = Object.freeze({ @@ -14,7 +13,6 @@ const TARGET_STYLE_BY_MODE = Object.freeze({ thermal: 'thermal', anime: 'anime', noir: 'noir', - snow: 'snow', }); const COCKPIT_MODE_BY_STYLE = Object.freeze({ @@ -24,7 +22,6 @@ const COCKPIT_MODE_BY_STYLE = Object.freeze({ thermal: 'thermal', anime: 'anime', noir: 'noir', - snow: 'snow', }); /** Normalize a requested Cockpit vision mode to the unfiltered Normal entry. */ diff --git a/src/cockpitVisionPolicy.test.mjs b/src/cockpitVisionPolicy.test.mjs index 3e5607543..be1ebceab 100644 --- a/src/cockpitVisionPolicy.test.mjs +++ b/src/cockpitVisionPolicy.test.mjs @@ -23,7 +23,6 @@ test('Cockpit vision order mirrors the unique map preset order', () => { 'thermal', 'anime', 'noir', - 'snow', ]); assert.equal(normalizeCockpitVisionMode('none'), 'optical'); assert.equal(normalizeCockpitVisionMode('unknown'), 'optical'); @@ -36,7 +35,7 @@ test('Cockpit entry selects the vision mode equivalent to the active map style', assert.equal(cockpitVisionModeForStyle('thermal'), 'thermal'); assert.equal(cockpitVisionModeForStyle('anime'), 'anime'); assert.equal(cockpitVisionModeForStyle('noir'), 'noir'); - assert.equal(cockpitVisionModeForStyle('snow'), 'snow'); + assert.equal(cockpitVisionModeForStyle('snow'), 'optical'); assert.equal(cockpitVisionModeForStyle('unsupported'), 'optical'); }); diff --git a/src/data/trafficPresetStyle.js b/src/data/trafficPresetStyle.js index 2f16a4b33..bac391232 100644 --- a/src/data/trafficPresetStyle.js +++ b/src/data/trafficPresetStyle.js @@ -20,7 +20,7 @@ * jam keeps a size bump so queues read as fat beads. * - `crt` (retro): saturated primaries that survive 10-level posterization * plus an all-bucket size boost to out-shout the pixel grid. - * - `normal` (normal/anime/snow/unknown): every lookup returns null/0 — + * - `normal` (normal/anime/unknown): every lookup returns null/0 — * the shipped palette and sizing apply untouched. * * Sim/uncovered dots (bucket null/'sim') are NEVER restyled under any diff --git a/src/data/trafficPresetStyle.test.mjs b/src/data/trafficPresetStyle.test.mjs index 7dd88e1a0..a05040a66 100644 --- a/src/data/trafficPresetStyle.test.mjs +++ b/src/data/trafficPresetStyle.test.mjs @@ -13,7 +13,7 @@ const luma = ([r, g, b]) => (0.299 * r + 0.587 * g + 0.114 * b) / 255; const BUCKETS = ['free', 'slow', 'jam']; const MONO_STYLES = ['surveillance', 'thermal', 'noir']; -const NORMAL_STYLES = ['normal', 'anime', 'snow', 'does-not-exist', '', undefined, null]; +const NORMAL_STYLES = ['normal', 'anime', 'does-not-exist', '', undefined, null]; test('profile mapping: NVG/FLIR/noir are mono, retro is crt, everything else normal', () => { for (const s of MONO_STYLES) assert.equal(trafficStyleProfile(s), 'mono', s); diff --git a/src/data/transitPresetStyle.js b/src/data/transitPresetStyle.js index 8c79d7c05..5996d4d97 100644 --- a/src/data/transitPresetStyle.js +++ b/src/data/transitPresetStyle.js @@ -18,7 +18,7 @@ * black sprite with a light ring is what a hot object looks like there. * - `crt` (retro): saturated per-mode colours that survive posterisation, * a size boost to out-shout the pixel grid, and a thinner halo. - * - `normal` (normal/anime/snow/unknown): every lookup returns the shipped + * - `normal` (normal/anime/unknown): every lookup returns the shipped * value — mode colour, shipped size, hairline edge only. * * Brackets are a different matter. The detection canvas composites ABOVE the diff --git a/src/data/transitPresetStyle.test.mjs b/src/data/transitPresetStyle.test.mjs index fc8e81323..7cbb2dcbf 100644 --- a/src/data/transitPresetStyle.test.mjs +++ b/src/data/transitPresetStyle.test.mjs @@ -17,7 +17,6 @@ test('profile mapping: NVG/FLIR/noir and cockpit nvg are mono, retro is crt, eve for (const plain of [ 'normal', 'anime', - 'snow', 'minecraft', '', null, diff --git a/src/director/document.test.mjs b/src/director/document.test.mjs index c0b8b0935..9294abb7b 100644 --- a/src/director/document.test.mjs +++ b/src/director/document.test.mjs @@ -135,6 +135,15 @@ test('document byte, nesting, collection, finite-number and string bounds are en ); }); +test('a shot saved with the retired Snow style migrates to Normal', () => { + const project = fixture(); + project.scenes[0].shots[0].visual = { style: 'snow' }; + const migrated = normalizeProject( + parseSceneDocument(JSON.stringify(project)), + ); + assert.equal(migrated.scenes[0].shots[0].visual.style, 'normal'); +}); + test('captured scope and extended detection edits survive migration', () => { const project = fixture(); project.scenes[0].shots[0].visual = { diff --git a/src/firstRunExperience.test.mjs b/src/firstRunExperience.test.mjs index d76a48458..6629536e2 100644 --- a/src/firstRunExperience.test.mjs +++ b/src/firstRunExperience.test.mjs @@ -1040,6 +1040,19 @@ test('the voice TOOL SCHEMA matches the pinned contract — the mission mapping 'rotate_globe', 'stop', ]); + // Snow is retired from set_visual_style; restore it to keep the legacy pin. + const visualStyle = legacyTools.find( + (tool) => tool.name === 'set_visual_style', + ).parameters.properties.style; + assert.deepEqual(visualStyle.enum, [ + 'normal', + 'retro', + 'surveillance', + 'thermal', + 'anime', + 'noir', + ]); + visualStyle.enum = [...visualStyle.enum, 'snow']; const block = JSON.stringify(legacyTools); // Re-derived for the voice layer manifest (generated layer enums, alias and // field hints), point-and-ask's pointer/referent arguments and the prompt diff --git a/src/hud.js b/src/hud.js index 011b53691..023d617dc 100644 --- a/src/hud.js +++ b/src/hud.js @@ -76,7 +76,6 @@ const MODE_LABELS = Object.freeze({ thermal: msg('FLIR'), anime: msg('ANIME'), noir: msg('NOIR'), - snow: msg('SNOW'), }); /** Region ids from _regionLabel() and the name shown for each. */ diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 3bbe4d404..8cbdf6ab5 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -66,8 +66,6 @@ "Anime": "Anime", "Apply high-contrast monochrome film-noir grading.": "Aplica una gradación monocroma de alto contraste estilo film noir.", "Noir": "Noir", - "Add a cold, snowy whiteout treatment to the scene.": "Añade a la escena un tratamiento frío de ventisca blanca.", - "Snow": "Nieve", "MAP SOURCE": "FUENTE DEL MAPA", "Map source": "Fuente del mapa", "Style": "Estilo", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 4594275cc..502b50605 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -1700,7 +1700,6 @@ "Thermal": "Termico", "Anime": "Anime", "Noir": "Noir", - "Snow": "Neve", "Current cockpit vision style: {style}. Activate for next style.": "Stile di visione attuale del cockpit: {style}. Attiva per lo stile successivo.", "Current style: {style} — click for next": "Stile attuale: {style}. Clic per il successivo", "Collapse Contact panel": "Comprimi il pannello Contatto", @@ -2781,7 +2780,6 @@ "Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "Simula il contrasto termico in stile FLIR. Alza Ironbow per il colore.", "Apply bright cel-shaded color and illustrated outlines.": "Applica colori vivaci cel-shaded e contorni illustrati.", "Apply high-contrast monochrome film-noir grading.": "Applica un color grading noir monocromatico ad alto contrasto.", - "Add a cold, snowy whiteout treatment to the scene.": "Aggiungi alla scena un effetto freddo di nevicata e whiteout.", "MAP SOURCE": "FONTE MAPPA", "Map source": "Fonte mappa", "Style": "Stile", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 087aeb106..f139e517a 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -1700,7 +1700,6 @@ "Thermal": "Térmico", "Anime": "Anime", "Noir": "Noir", - "Snow": "Neve", "Current cockpit vision style: {style}. Activate for next style.": "Estilo atual de visão da cabine: {style}. Ative para o próximo estilo.", "Current style: {style} — click for next": "Estilo atual: {style} — clique para o próximo", "Collapse Contact panel": "Recolher painel de contato", @@ -2783,7 +2782,6 @@ "Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "Simular contraste térmico estilo FLIR. Aumente o Ironbow para ter cor.", "Apply bright cel-shaded color and illustrated outlines.": "Aplicar cores vivas em cel shading e contornos ilustrados.", "Apply high-contrast monochrome film-noir grading.": "Aplicar gradação monocromática de filme noir com alto contraste.", - "Add a cold, snowy whiteout treatment to the scene.": "Adicionar à cena um efeito frio de nevasca branca.", "MAP SOURCE": "FONTE DO MAPA", "Map source": "Fonte do mapa", "Style": "Estilo", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index fdb5066c7..0e8632bf3 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -416,7 +416,6 @@ "Thermal": "Тепловизор", "Anime": "Аниме", "Noir": "Нуар", - "Snow": "Снег", "Current cockpit vision style: {style}. Activate for next style.": "Текущий стиль обзора кабины: {style}. Активируйте, чтобы переключить на следующий.", "Current style: {style} — click for next": "Текущий стиль: {style} — нажмите для следующего", "AIRCRAFT": "САМОЛЁТ", @@ -690,7 +689,6 @@ "Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "Имитирует тепловой контраст в стиле FLIR. Для цвета увеличьте Ironbow.", "Apply bright cel-shaded color and illustrated outlines.": "Применяет яркие cel-shading-цвета и рисованные контуры.", "Apply high-contrast monochrome film-noir grading.": "Применяет высококонтрастную монохромную цветокоррекцию в стиле нуар.", - "Add a cold, snowy whiteout treatment to the scene.": "Добавляет в сцену холодный снежный эффект «белой мглы».", "Expand LOCATION": "Развернуть панель «Место»", "Collapse panel": "Свернуть панель", "Pin location tray": "Закрепить панель места", diff --git a/src/i18n/locales/uk.json b/src/i18n/locales/uk.json index 338a4fcc7..2e6fa95c0 100644 --- a/src/i18n/locales/uk.json +++ b/src/i18n/locales/uk.json @@ -1723,7 +1723,6 @@ "Thermal": "Тепловізор", "Anime": "Аніме", "Noir": "Нуар", - "Snow": "Сніг", "Current cockpit vision style: {style}. Activate for next style.": "Поточний візуальний стиль у кабіні: {style}. Активуйте, щоб перейти до наступного.", "Current style: {style} — click for next": "Поточний стиль: {style} — клацніть для наступного", "Collapse Contact panel": "Згорнути панель контактів", @@ -2850,7 +2849,6 @@ "Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "Імітує тепловий контраст у стилі FLIR. Для кольору збільште Ironbow.", "Apply bright cel-shaded color and illustrated outlines.": "Застосовує яскраву мультяшну заливку та ілюстровані контури.", "Apply high-contrast monochrome film-noir grading.": "Застосовує висококонтрастну чорно-білу обробку в стилі нуар.", - "Add a cold, snowy whiteout treatment to the scene.": "Додає до сцени холодний ефект снігової імли.", "MAP SOURCE": "ДЖЕРЕЛО КАРТИ", "Map source": "Джерело карти", "Style": "Стиль", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 5045bcaeb..06fa38a18 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1626,7 +1626,6 @@ "Thermal": "热成像", "Anime": "动漫", "Noir": "黑白", - "Snow": "雪景", "Current cockpit vision style: {style}. Activate for next style.": "当前座舱视觉样式:{style}。点击可切换到下一个样式。", "Current style: {style} — click for next": "当前样式:{style}——点击切换到下一个", "Collapse Contact panel": "收起目标面板", @@ -2537,7 +2536,6 @@ "Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "模拟 FLIR 风格的热成像对比。调高 Ironbow 可显示色彩。", "Apply bright cel-shaded color and illustrated outlines.": "应用明亮的赛璐珞色彩和插画轮廓线。", "Apply high-contrast monochrome film-noir grading.": "应用高对比度的黑白电影风格调色。", - "Add a cold, snowy whiteout treatment to the scene.": "为场景添加寒冷的暴雪白化效果。", "MAP SOURCE": "地图源", "Map source": "地图源", "Style": "样式", diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index 6ba91fced..d594f0212 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -1616,7 +1616,6 @@ "Thermal": "熱成像", "Anime": "動漫", "Noir": "黑白", - "Snow": "雪景", "Current cockpit vision style: {style}. Activate for next style.": "當前座艙視覺樣式:{style}。點擊可切換到下一個樣式。", "Current style: {style} — click for next": "當前樣式:{style}——點擊切換到下一個", "Collapse Contact panel": "收起目標面板", @@ -2525,7 +2524,6 @@ "Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "模擬 FLIR 風格的熱成像對比。調高 Ironbow 可顯示色彩。", "Apply bright cel-shaded color and illustrated outlines.": "應用明亮的賽璐珞色彩和插畫輪廓線。", "Apply high-contrast monochrome film-noir grading.": "應用高對比度的黑白電影風格調色。", - "Add a cold, snowy whiteout treatment to the scene.": "為場景添加寒冷的暴雪白化效果。", "MAP SOURCE": "地圖源", "Map source": "地圖源", "Style": "樣式", diff --git a/src/radioMarkup.test.mjs b/src/radioMarkup.test.mjs index dfe5b4257..9d9ae45ed 100644 --- a/src/radioMarkup.test.mjs +++ b/src/radioMarkup.test.mjs @@ -239,6 +239,19 @@ test('no unchanged Realtime tool definition drifts silently', () => { 'Direct the camera like a drone operator: orbit the current view target, pan, tilt, or rotate — one bounded nudge (mode=once) or continuous motion until stopped (mode=continuous). Continuous motion also stops on any manual camera input or when a navigation tool runs. Say the RESULTING state when confirming ("Orbiting slowly").'; camera.parameters.properties.direction.description = 'Required except for orbit (defaults right/clockwise) and stop.'; + // Snow is retired from set_visual_style; restore it to keep the legacy pin. + const visualStyle = unchanged.find( + (tool) => tool.name === 'set_visual_style', + ).parameters.properties.style; + assert.deepEqual(visualStyle.enum, [ + 'normal', + 'retro', + 'surveillance', + 'thermal', + 'anime', + 'noir', + ]); + visualStyle.enum = [...visualStyle.enum, 'snow']; assert.equal(unchanged.length, 14); const digest = createHash('sha256') .update(JSON.stringify(unchanged)) diff --git a/src/scenes/project.js b/src/scenes/project.js index eb3084962..f2297afaf 100644 --- a/src/scenes/project.js +++ b/src/scenes/project.js @@ -7,6 +7,7 @@ import { BLOOM_SCALE_VERSION, decodeBloomIntensity, } from '../bloom.js'; +import { RETIRED_STYLES } from '../visualStylePolicy.js'; /** @constant {number} Current schema version for project migration */ export const PROJECT_VERSION = SCENE_DOCUMENT_VERSION; @@ -262,7 +263,9 @@ export function normalizeShot( ...(rawShot?.dataPackIds ? { dataPackIds: [...rawShot.dataPackIds] } : {}), ...(rawShot?.move ? { move: deepClone(rawShot.move) } : {}), visual: { - style: visual.style || 'normal', + style: RETIRED_STYLES.includes(visual.style) + ? 'normal' + : visual.style || 'normal', bloom: normalizeBloomState(bloom, { projectVersion, fallbackIntensity: 50, diff --git a/src/share/legacyShareState.test.mjs b/src/share/legacyShareState.test.mjs index cde3a9ed6..f48c7adeb 100644 --- a/src/share/legacyShareState.test.mjs +++ b/src/share/legacyShareState.test.mjs @@ -107,6 +107,13 @@ test('the HAL example survives the round trip and shrinks to 39 characters', () ); }); +test('a long-form link to the retired Snow style opens as Normal', () => { + const snow = parseLegacy('v=2&lat=1&lon=2&style=snow&sp=d.90_w.20'); + const normal = parseLegacy('v=2&lat=1&lon=2'); + assert.equal(snow.style, 'normal'); + assert.deepEqual(snow.styleParams, normal.styleParams); +}); + test('links from earlier long-form versions keep their historical meaning', () => { for (const hash of [ 'lat=10&lon=20', diff --git a/src/share/viewCodec.js b/src/share/viewCodec.js index 84ba8fbc1..b72f79d34 100644 --- a/src/share/viewCodec.js +++ b/src/share/viewCodec.js @@ -31,6 +31,7 @@ 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, STYLE_PRESET_DEFAULTS, @@ -101,7 +102,7 @@ export const SHARE_VIEW_ENUMS = Object.freeze({ 'thermal', 'anime', 'noir', - 'snow', + 'snow', // retired (RETIRED_STYLES): decodes as Normal ]), mapStack: Object.freeze([ 'photoreal', @@ -221,10 +222,6 @@ export const SHARE_VIEW_STYLE_PARAMS = Object.freeze({ { key: 'grainAmt', token: 'g', min: 0, max: 1 }, { key: 'vignetteAmt', token: 'v', min: 0, max: 1 }, ]), - snow: freezeAll([ - { key: 'density', token: 'd', min: 0, max: 1 }, - { key: 'wind', token: 'w', min: 0, max: 1 }, - ]), }); /** @@ -1321,8 +1318,12 @@ export function decodeShareView(code, { nowMs = Date.now() } = {}) { return { ok: false, reason: 'out-of-range' }; } + // A retired style's code stays reserved: the view opens on Normal, and + // parameters written for it are dropped rather than reported as unknown. + const retiredStyle = RETIRED_STYLES.includes(raw.style); if (raw.style !== undefined && !VIEW_STYLES.includes(raw.style)) { - if (get('style').len) unknown.code('enum:style', raw.style); + if (get('style').len && !retiredStyle) + unknown.code('enum:style', raw.style); raw.style = undefined; } for (const key of ['hudVariant', 'mapStack']) { @@ -1357,7 +1358,7 @@ export function decodeShareView(code, { nowMs = Date.now() } = {}) { unknown.code('layer-option', 'without-layers'); if (get('panelState')) raw.panelState = decodePanels(get('panelState').len, unknown); - if (get('styleParams')) + if (get('styleParams') && !retiredStyle) raw.styleParams = decodeStyleParams( get('styleParams').len, raw.style ?? 'normal', diff --git a/src/share/viewCodec.test.mjs b/src/share/viewCodec.test.mjs index 4ec2b967c..4449ceb19 100644 --- a/src/share/viewCodec.test.mjs +++ b/src/share/viewCodec.test.mjs @@ -2,6 +2,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { LAYER_STATE_REGISTRY, + LAYER_STATE_TOKEN_ALPHABET, LAYER_STATE_TOKEN_RESERVATIONS, REGISTERED_LAYER_IDS, createDefaultLayerState, @@ -440,7 +441,8 @@ test('defaults travel as frozen meanings that match today’s app defaults', () assert.deepEqual(SHARE_VIEW_ENUMS.hudVariant.slice(0, HUD_LAYOUTS.length), [ ...HUD_LAYOUTS, ]); - assert.deepEqual(SHARE_VIEW_ENUMS.style, [...VIEW_STYLES]); + // Snow is retired; its code stays reserved (see the retired-style test). + assert.deepEqual(SHARE_VIEW_ENUMS.style, [...VIEW_STYLES, 'snow']); assert.deepEqual(SHARE_VIEW_ENUMS.mapStack, [...VIEW_MAPS]); assert.deepEqual(SHARE_VIEW_ENUMS.detectionAllocation, [ ...ALLOCATION_STRATEGIES, @@ -549,7 +551,6 @@ test('the field registry is well formed and permanent', () => { ...common, styleParams: { contrastAmt: 1.2, grainAmt: 0.5, vignetteAmt: 0.5 }, }, - snow: { ...common, styleParams: { density: 0.6, wind: 0.5 } }, }); assert.deepEqual( SHARE_VIEW_ENTITY_KINDS.map((e) => [e.code, e.kind]), @@ -966,6 +967,29 @@ test('enum names outside the code tables travel as text', () => { ]); }); +test('links to the retired Snow style open as Normal', () => { + const snowCode = SHARE_VIEW_ENUMS.style.indexOf('snow'); + assert.equal(snowCode, 6, 'a retired code is never reused'); + const params = new WireWriter() + .varint(LAYER_STATE_TOKEN_ALPHABET.indexOf('d')) + .varint(90) + .varint(LAYER_STATE_TOKEN_ALPHABET.indexOf('w')) + .varint(20) + .finish(); + for (const writeStyle of [ + (writer) => writer.uintField(14, snowCode), + (writer) => writer.textField(14, 'snow'), + ]) { + const writer = new WireWriter(); + writer.bytes.push(1); + writeStyle(writer); + writer.bytesField(27, params); + const { state, unknownFields } = decodeOk(hashOf(writer.finish())); + assert.deepEqual(state, normalizeShareViewState({})); + assert.equal(unknownFields, undefined); + } +}); + /* ---------------- integrity ---------------- */ test('malformed input is rejected with a reason, never thrown', () => { diff --git a/src/styles/snow.js b/src/styles/snow.js deleted file mode 100644 index a4db0a029..000000000 --- a/src/styles/snow.js +++ /dev/null @@ -1,97 +0,0 @@ -import { msg } from '../i18n/marker.js'; - -/** - * Snow Style — White overlay + Desaturation + Falling particles - * Transforms the world into a winter wonderland - */ -export const snowShader = { - name: 'snow', - uniforms: { - density: { default: 0.6, min: 0, max: 1, label: msg('Density') }, - wind: { default: 0.5, min: 0, max: 1, label: msg('Wind') }, - }, - fragmentShader: /* glsl */ ` - uniform sampler2D colorTexture; - uniform vec2 colorTextureDimensions; - uniform float intensity; - uniform float time; - uniform float density; - uniform float wind; - in vec2 v_textureCoordinates; - - // Hash function for pseudo-random snow - float hash(vec2 p) { - p = fract(p * vec2(123.34, 456.21)); - p += dot(p, p + 45.32); - return fract(p.x * p.y); - } - - // Layered snow particles - float snowLayer(vec2 uv, float layer) { - float depth = 0.5 + layer * 0.5; - float speed = 0.4 + layer * 0.3; - float size = mix(0.01, 0.025, layer); - float windForce = 0.05 + wind * 0.2; - - vec2 snowUV = uv * vec2(1.0, 0.5) * (4.0 + layer * 4.0); - snowUV.y += time * speed; - // Offset is the integral of a swaying velocity sin(time * 0.5 + phase) * windForce, - // so the sway stays bounded however long the app has been running. - snowUV.x -= 2.0 * cos(time * 0.5 + layer * 3.14) * windForce; - - vec2 cell = floor(snowUV); - vec2 f = fract(snowUV); - - float snow = 0.0; - // Check neighboring cells for smooth particles - for (int y = -1; y <= 1; y++) { - for (int x = -1; x <= 1; x++) { - vec2 neighbor = vec2(float(x), float(y)); - vec2 point = vec2(hash(cell + neighbor), hash(cell + neighbor + 100.0)); - // Animate particle position slightly - point = 0.5 + 0.4 * sin(time * 0.3 + 6.2831 * point); - float d = length(f - neighbor - point); - snow += smoothstep(size, 0.0, d) * depth; - } - } - return snow; - } - - void main() { - vec2 uv = v_textureCoordinates; - vec4 color = texture(colorTexture, uv); - - // Cool/blue color shift - vec3 cool = color.rgb * vec3(0.85, 0.9, 1.1); - - // Desaturate partially - float luma = dot(cool, vec3(0.299, 0.587, 0.114)); - vec3 desaturated = mix(cool, vec3(luma), 0.4 * intensity); - - // Brighten (snow coverage lightens everything) - vec3 brightened = mix(desaturated, vec3(1.0), 0.15 * intensity); - - // Add white frost overlay on brighter areas - float frostMask = smoothstep(0.3, 0.8, luma); - vec3 frosted = mix(brightened, vec3(0.95, 0.97, 1.0), frostMask * 0.25 * intensity); - - // Accumulate snow particle layers (density controls amount) - float snow = 0.0; - snow += snowLayer(uv, 0.0) * 0.5; - snow += snowLayer(uv, 0.25) * 0.6; - snow += snowLayer(uv, 0.5) * 0.7; - snow += snowLayer(uv, 0.75) * 0.8; - snow += snowLayer(uv, 1.0) * 1.0; - snow = clamp(snow * (0.4 + density * 1.2), 0.0, 1.0); - - // Composite snow particles - vec3 result = frosted + snow * 0.8 * intensity; - - // Subtle fog in lower areas - float fog = smoothstep(0.0, 0.4, 1.0 - uv.y) * 0.1 * intensity; - result = mix(result, vec3(0.85, 0.88, 0.95), fog); - - out_FragColor = vec4(mix(color.rgb, result, intensity), color.a); - } - `, -}; diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index 4b999a75f..1ff3b45ac 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -1598,7 +1598,7 @@ export class StyleManager extends ShellFacade { * 2. Crossfades the new shader stage intensity to 1. * 3. Applies style preset defaults (bloom/sharpen/HUD) if applyPreset is true. * 4. Updates button highlights, style indicator, slider panel, HUD, and detection overlay. - * @param {string} styleName - Target style ('normal'|'retro'|'surveillance'|'thermal'|'anime'|'noir'|'snow'). + * @param {string} styleName - Target style ('normal'|'retro'|'surveillance'|'thermal'|'anime'|'noir'). * @param {object} [options] * @param {boolean} [options.applyPreset=true] - Whether to apply STYLE_PRESET_DEFAULTS for the new style. * @param {boolean} [options.userInitiated=false] - Whether a direct user preset choice owns the Cyber exit style. diff --git a/src/ui/applicationShortcuts.js b/src/ui/applicationShortcuts.js index 010b6b620..57214a570 100644 --- a/src/ui/applicationShortcuts.js +++ b/src/ui/applicationShortcuts.js @@ -5,7 +5,6 @@ const STYLE_KEYS = Object.freeze({ 4: 'thermal', 5: 'anime', 6: 'noir', - 7: 'snow', }); /** diff --git a/src/ui/cockpitInstruments.js b/src/ui/cockpitInstruments.js index 77a4bea7d..a4f843c4c 100644 --- a/src/ui/cockpitInstruments.js +++ b/src/ui/cockpitInstruments.js @@ -268,7 +268,6 @@ export function setVisionMode(mode, { revealParameters = false } = {}) { thermal: t('FLIR'), anime: t('ANIME'), noir: t('NOIR'), - snow: t('SNOW'), }; const names = { optical: t('Normal'), @@ -277,7 +276,6 @@ export function setVisionMode(mode, { revealParameters = false } = {}) { thermal: t('Thermal'), anime: t('Anime'), noir: t('Noir'), - snow: t('Snow'), }; if (this.visionCurrent) { this.visionCurrent.dataset.cockpitVision = next; diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs index 03ee75f08..f316d219c 100644 --- a/src/ui/cyberTheme.test.mjs +++ b/src/ui/cyberTheme.test.mjs @@ -105,7 +105,6 @@ test('visual styles retain Cyber through real preset application without droppin 'normal', 'noir', 'anime', - 'snow', 'thermal', 'retro', ]) { @@ -162,8 +161,8 @@ test('a preset explicitly selected in Cyber survives exit and resets for re-entr owner.setStyle('noir'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); - owner.setStyle('snow', { userInitiated: true }); - assert.equal(owner.activeStyle, 'snow'); + owner.setStyle('anime', { userInitiated: true }); + assert.equal(owner.activeStyle, 'anime'); assert.equal(owner._preCyberStyle, 'noir'); assert.equal(owner._cyberStyleUserSelected, true); @@ -172,16 +171,16 @@ test('a preset explicitly selected in Cyber survives exit and resets for re-entr assert.equal(owner._preCyberStyle, 'noir'); owner._setHudVariant('operator', { applyVisualDefaults: true }); assert.equal(hud.getVariant(), 'operator'); - assert.equal(owner.activeStyle, 'snow'); + assert.equal(owner.activeStyle, 'anime'); assert.equal(owner._preCyberStyle, null); assert.equal(owner._cyberStyleUserSelected, false); // A later untouched Cyber round trip restores the new pre-Cyber preset. owner._setHudVariant('cyber', { applyVisualDefaults: true }); assert.equal(owner.activeStyle, 'thermal'); - assert.equal(owner._preCyberStyle, 'snow'); + assert.equal(owner._preCyberStyle, 'anime'); owner._setHudVariant('minimal', { applyVisualDefaults: true }); - assert.equal(owner.activeStyle, 'snow'); + assert.equal(owner.activeStyle, 'anime'); }); test('an explicit same-value FLIR choice is preserved on Cyber exit', (t) => { @@ -200,15 +199,15 @@ test('an authoritative same-Cyber reset clears local preset ownership', (t) => { const { owner } = styleOwner(t, 'tactical'); owner.setStyle('noir'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); - owner.setStyle('snow', { userInitiated: true }); + owner.setStyle('anime', { userInitiated: true }); assert.equal(owner._cyberStyleUserSelected, true); owner._setHudVariant('cyber'); - assert.equal(owner.activeStyle, 'snow'); + assert.equal(owner.activeStyle, 'anime'); assert.equal(owner._preCyberStyle, null); assert.equal(owner._cyberStyleUserSelected, false); owner._setHudVariant('operator', { applyVisualDefaults: true }); - assert.equal(owner.activeStyle, 'snow'); + assert.equal(owner.activeStyle, 'anime'); }); test('Display and voice HUD routes preserve explicit Cyber entry and exit authority', async (t) => { @@ -226,10 +225,10 @@ test('Display and voice HUD routes preserve explicit Cyber entry and exit author const display = makeOwner(); const hudLayout = new EventTarget(); hudLayout.value = 'cyber'; - const snowButton = new EventTarget(); - snowButton.dataset = { style: 'snow' }; + const animeButton = new EventTarget(); + animeButton.dataset = { style: 'anime' }; const controls = bindDisplayControls({ - elements: { hudLayout, styleButtons: [snowButton] }, + elements: { hudLayout, styleButtons: [animeButton] }, actions: { setHudLayout: (value) => display.owner.setHudLayout(value), setStyle: (value) => @@ -239,12 +238,12 @@ test('Display and voice HUD routes preserve explicit Cyber entry and exit author hudLayout.dispatchEvent(new Event('change')); assert.equal(display.hud.getVariant(), 'cyber'); assert.equal(display.owner.activeStyle, 'thermal'); - snowButton.dispatchEvent(new Event('click')); - assert.equal(display.owner.activeStyle, 'snow'); + animeButton.dispatchEvent(new Event('click')); + assert.equal(display.owner.activeStyle, 'anime'); hudLayout.value = 'operator'; hudLayout.dispatchEvent(new Event('change')); assert.equal(display.hud.getVariant(), 'operator'); - assert.equal(display.owner.activeStyle, 'snow'); + assert.equal(display.owner.activeStyle, 'anime'); controls.destroy(); const voice = makeOwner(); @@ -263,13 +262,13 @@ test('Display and voice HUD routes preserve explicit Cyber entry and exit author assert.equal(entered.ok, true); assert.equal(voice.hud.getVariant(), 'cyber'); assert.equal(voice.owner.activeStyle, 'thermal'); - const selected = await runVoiceAction('set_visual_style', { style: 'snow' }); + const selected = await runVoiceAction('set_visual_style', { style: 'anime' }); assert.equal(selected.ok, true); - assert.equal(voice.owner.activeStyle, 'snow'); + assert.equal(voice.owner.activeStyle, 'anime'); const exited = await runVoiceAction('set_hud', { layout: 'operator' }); assert.equal(exited.ok, true); assert.equal(voice.hud.getVariant(), 'operator'); - assert.equal(voice.owner.activeStyle, 'snow'); + assert.equal(voice.owner.activeStyle, 'anime'); }); test('scope choice and expanded feather survive every HUD layout and both restore paths', async (t) => { @@ -301,11 +300,18 @@ test('scope choice and expanded feather survive every HUD layout and both restor assert.equal(owner._scopeFeatherSlider.value, '200'); }); +test('a scene shot saved with the retired Snow style plays on Normal', async (t) => { + const { owner } = styleOwner(t, 'tactical'); + owner.setStyle('noir'); + await owner.applyVisualState({ style: 'snow' }); + assert.equal(owner.activeStyle, 'normal'); +}); + test('scene and share HUD transitions do not restore a local pre-Cyber preset', async (t) => { const { owner, hud } = styleOwner(t, 'tactical'); owner.setStyle('noir'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); - owner.setStyle('snow', { userInitiated: true }); + owner.setStyle('anime', { userInitiated: true }); assert.equal(owner._cyberStyleUserSelected, true); await owner.applyVisualState({ style: 'retro', hud: { variant: 'cyber' } }); @@ -319,7 +325,7 @@ test('scene and share HUD transitions do not restore a local pre-Cyber preset', owner.setStyle('surveillance'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); assert.equal(owner._preCyberStyle, 'surveillance'); - owner.setStyle('snow', { userInitiated: true }); + owner.setStyle('anime', { userInitiated: true }); await owner.restoreShareState({ style: 'thermal', hudVariant: 'cyber', diff --git a/src/ui/scenePresentation.js b/src/ui/scenePresentation.js index 52e99796e..eb1800e75 100644 --- a/src/ui/scenePresentation.js +++ b/src/ui/scenePresentation.js @@ -12,7 +12,6 @@ const STYLE_LABELS = Object.freeze({ thermal: msg('FLIR'), anime: msg('ANIME'), noir: msg('NOIR'), - snow: msg('SNOW'), }); const DETECTION_MODE_LABELS = Object.freeze({ diff --git a/src/ui/styles/cyber.css b/src/ui/styles/cyber.css index e25bbae2d..f19bb3315 100644 --- a/src/ui/styles/cyber.css +++ b/src/ui/styles/cyber.css @@ -747,12 +747,6 @@ .btn-icon::before { content: '◐'; } -:root[data-ui-theme='cyber'] - #command-dock - .style-btn[data-style='snow'] - .btn-icon::before { - content: '❄︎'; -} :root[data-ui-theme='cyber'] :where(#bloom-toggle, #sharpen-toggle) { font-size: 0; diff --git a/src/ui/templates/command-dock.html b/src/ui/templates/command-dock.html index 3dac977ae..fa066f9da 100644 --- a/src/ui/templates/command-dock.html +++ b/src/ui/templates/command-dock.html @@ -43,11 +43,6 @@ Noir 6 -
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/actionSchemas.test.mjs b/src/voice/actionSchemas.test.mjs index 97e5004b5..3bf9cfcd9 100644 --- a/src/voice/actionSchemas.test.mjs +++ b/src/voice/actionSchemas.test.mjs @@ -45,7 +45,8 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea // S2 GhostMaps extends the generated layer enums. // Panel adds named voice targets and timeout; shared Panel/Cockpit guidance // is consolidated without changing either feature's arguments. - '9ee82c4214499280c208c93751b19d7b724c81b7727a2d0c6dab0184749fbe15', + // Snow leaves the set_visual_style enum. + '724a82b538a12a994401fc941e175625b032bfb96109934e091d3ef5b9600909', ); }); @@ -169,6 +170,18 @@ test('all legacy action arguments are byte-identical after removing the delibera // Cyber adds one HUD layout. const hud = property('set_hud').layout; hud.enum = hud.enum.filter((layout) => layout !== 'cyber'); + // Snow is retired from set_visual_style; restore it to keep the legacy pin. + const visualStyle = legacy.find((tool) => tool.name === 'set_visual_style') + .parameters.properties.style; + assert.deepEqual(visualStyle.enum, [ + 'normal', + 'retro', + 'surveillance', + 'thermal', + 'anime', + 'noir', + ]); + visualStyle.enum = [...visualStyle.enum, 'snow']; // Derived by applying the same removals to the 4b56d0e9 actionSchemas. assert.equal( createHash('sha256').update(JSON.stringify(legacy)).digest('hex'), 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'],