From 1da65dc2645e1f783cc5e125dd1ea02c455d3c52 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Mon, 2 Nov 2026 11:29:55 +0400 Subject: [PATCH] feat(ui): switch between Cesium and Navara in Visual Presets Remember the chosen engine and restore the current view after navigation. Keep the standalone JavaScript UI and retire the Dioxus migration and Cesium removal milestones. --- README.md | 2 + scripts/format-scope.json | 1 + scripts/qa-engine-switch.mjs | 112 +++++++++++++++++++++++++++++ src/sharelink.js | 6 ++ src/standalone/controls.js | 34 ++++++++- src/ui/styles/controls.css | 9 +++ src/ui/templates/command-dock.html | 8 +++ 7 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 scripts/qa-engine-switch.mjs diff --git a/README.md b/README.md index 9ce8d1ff0..c9db8db5d 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,8 @@ Choose a first-run mission, or try these in order. The GIFs show Google Photorea 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."_ +Open **Visual Presets → Globe Engine** to switch between Cesium and experimental Navara. The page reloads at the current view and remembers your choice. A link with `?engine=cesium` or `?engine=navara` overrides the saved choice. A fresh visit starts with Cesium. + **Keyboard:** `1`–`7` visual styles · `H` HUD · `D` detection · `C` cockpit · `` ` `` frame rate · `Esc` out. Trackpad pinch zooms the globe. --- diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 65bfe7b72..4fe3f247e 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1080,6 +1080,7 @@ "build/navaraAssets.js", "scripts/qa-engine-bench.mjs", "scripts/qa-engine-shell.mjs", + "scripts/qa-engine-switch.mjs", "scripts/qa/navara-contract.html", "scripts/qa/navaraContractPage.js", "scripts/qa/tileCache.mjs", diff --git a/scripts/qa-engine-switch.mjs b/scripts/qa-engine-switch.mjs new file mode 100644 index 000000000..dbfb4d678 --- /dev/null +++ b/scripts/qa-engine-switch.mjs @@ -0,0 +1,112 @@ +import assert from 'node:assert/strict'; +import puppeteer from 'puppeteer'; +import { blockPaidQaMaps } from './qa-map-policy.mjs'; +import { installTileCache } from './qa/tileCache.mjs'; + +const base = new URL(process.argv[2]); +if ( + !['localhost', '127.0.0.1'].includes(base.hostname) || + ['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes( + base.port, + ) +) { + throw new Error('Supply an unreserved local preview URL'); +} +base.search = '?welcome=0&look=cinematic'; +base.hash = 'v=2&lat=42.3554&lon=-71.0605&alt=6000&heading=0&pitch=-90&map=osm'; +const browser = await puppeteer.launch({ + headless: true, + args: ['--use-angle=metal', '--enable-gpu'], +}); +try { + const page = await browser.newPage(); + await blockPaidQaMaps(page); + await installTileCache(page); + await page.evaluateOnNewDocument(() => { + localStorage.setItem('gev:first-run-mission:v1', 'suppressed'); + }); + await page.setViewport({ width: 1280, height: 800 }); + const errors = []; + page.on('pageerror', (error) => errors.push(error.message)); + async function ready(kind) { + await page.waitForFunction( + (kind) => + window.__godsEyeView?.engine?.kind === kind && + document.getElementById('loading-screen')?.classList.contains('hidden'), + { timeout: 90000 }, + kind, + ); + await page.evaluate(async () => { + await window.__godsEyeView.styleManager.initialRestorePromise; + if (window.__GOOGLE_MAPS_API_KEY__ || window.__godsEyeView.tileset) + throw new Error('Keyless preview required'); + }); + assert.equal(await page.$eval('#globe-engine', (el) => el.value), kind); + } + const stored = () => page.evaluate(() => localStorage.getItem('gev.engine')); + async function switchTo(kind) { + if ( + await page.$eval( + '#control-panel-toggle', + (el) => el.getAttribute('aria-expanded') !== 'true', + ) + ) { + await page.focus('#control-panel-toggle'); + await page.keyboard.press('Enter'); + } + await page.waitForSelector('#globe-engine', { visible: true }); + await Promise.all([ + page.waitForNavigation({ waitUntil: 'domcontentloaded' }), + page.select('#globe-engine', kind), + ]); + await ready(kind); + const url = new URL(page.url()); + assert.equal(url.searchParams.get('engine'), kind); + assert.equal(url.searchParams.get('look'), 'cinematic'); + const hash = new URLSearchParams(url.hash.slice(1)); + assert.equal(hash.get('map'), 'osm'); + assert.ok(Math.abs(Number(hash.get('lat')) - 42.3554) < 0.01); + assert.ok(Math.abs(Number(hash.get('lon')) + 71.0605) < 0.01); + assert.ok(Math.abs(Number(hash.get('alt')) - 6000) < 30); + const view = await page.evaluate(() => { + const { engine, mapStackController } = window.__godsEyeView; + const { latitude, longitude, height } = engine.camera.pose().cartographic; + return { + latitude: latitude * (180 / Math.PI), + longitude: longitude * (180 / Math.PI), + height, + map: mapStackController.getActiveId(), + }; + }); + assert.equal(view.map, 'osm'); + assert.ok(Math.abs(view.latitude - 42.3554) < 0.01); + assert.ok(Math.abs(view.longitude + 71.0605) < 0.01); + assert.ok(Math.abs(view.height - 6000) < 30); + } + await page.goto(base.href, { waitUntil: 'domcontentloaded' }); + await ready('cesium'); + await switchTo('navara'); + assert.equal(await stored(), 'navara'); + await page.goBack({ waitUntil: 'domcontentloaded' }); + await ready('cesium'); + await page.goto(base.href, { waitUntil: 'domcontentloaded' }); + await ready('navara'); + await switchTo('cesium'); + assert.equal(await stored(), 'cesium'); + await page.evaluate(() => { + const write = Storage.prototype.setItem; + Storage.prototype.setItem = function (key, value) { + if (key === 'gev.engine') + throw new DOMException('Blocked', 'SecurityError'); + return write.call(this, key, value); + }; + }); + await switchTo('navara'); + assert.equal(await stored(), 'cesium'); + assert.deepEqual(errors, []); + console.log( + 'PASS Cesium default, both switches, share pose/map, Back, remembered engine, blocked preference storage', + ); +} finally { + await browser.close(); +} diff --git a/src/sharelink.js b/src/sharelink.js index 028282d65..1178d49ad 100644 --- a/src/sharelink.js +++ b/src/sharelink.js @@ -522,6 +522,12 @@ export class ShareLinkManager { } } + /** Save the current view before leaving the page. */ + flush() { + clearTimeout(this._debounceTimer); + this._updateHash(); + } + _scheduleUpdate() { if (this._destroyed || this._initialRestorePending) return; clearTimeout(this._debounceTimer); diff --git a/src/standalone/controls.js b/src/standalone/controls.js index b168c5e1b..d1131525f 100644 --- a/src/standalone/controls.js +++ b/src/standalone/controls.js @@ -1,8 +1,40 @@ import { createApplicationControls } from '../app/controls.js'; import { getStandaloneCatalog } from './catalog.js'; +import { ENGINE_STORAGE_KEY } from '../app/engineChoice.js'; + export function createStandaloneControls(options) { - return createApplicationControls({ + const controls = createApplicationControls({ catalog: options?.catalog ?? getStandaloneCatalog(), ...options, }); + const select = document.getElementById('globe-engine'); + const container = document.getElementById('globe-engine-control'); + if (select && container) { + const current = options.scene.engine.kind; + select.value = current; + const switchEngine = () => { + const next = select.value; + if (next === current || !['cesium', 'navara'].includes(next)) return; + controls.styleManager.shareLinkManager.flush(); + const url = new URL(window.location.href); + // Keep Back meaningful even when this visit began without ?engine=. + url.searchParams.set('engine', current); + window.history.replaceState(window.history.state, '', url); + url.searchParams.set('engine', next); + try { + window.localStorage.setItem(ENGINE_STORAGE_KEY, next); + } catch { + // The explicit URL also works when storage is blocked. + } + select.value = current; + window.location.assign(url); + }; + select.addEventListener('change', switchEngine); + container.hidden = false; + options.defer(() => { + select.removeEventListener('change', switchEngine); + container.hidden = true; + }); + } + return controls; } diff --git a/src/ui/styles/controls.css b/src/ui/styles/controls.css index 8607cd459..9be9b936a 100644 --- a/src/ui/styles/controls.css +++ b/src/ui/styles/controls.css @@ -824,6 +824,15 @@ } /* Map Source tiles live with Visual Presets in the bottom command tray. */ +#globe-engine-control label { + gap: 8px; + margin-top: 8px; +} + +#globe-engine:focus-visible { + outline: 2px solid currentColor; +} + .map-source-section { display: grid; gap: 6px; diff --git a/src/ui/templates/command-dock.html b/src/ui/templates/command-dock.html index cf1af7efb..f98032a6a 100644 --- a/src/ui/templates/command-dock.html +++ b/src/ui/templates/command-dock.html @@ -49,6 +49,14 @@ 7 +
MAP SOURCE